【防止重复下单】分布式系统接口幂等性实现方案

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 【防止重复下单】分布式系统接口幂等性实现方案

问题背景


最容易想到的使用数据库事务。

比如创建订单时,要同时往订单表、订单商品表插入数据,那这些插入数据的INSERT必须在一个数据库事务执行,数据库事务可保证:执行这些INSERT语句,同生共死!


订单服务调用支付服务,不巧网络超时,然后订单服务走重试机制,给你重试了一把,支付服务收到一个支付请求两次,而且因为轮询负载均衡算法落在不同机器!

所以一个分布式系统中的接口,要如何保证幂等性呢?


如何避免重复下单?


评论里有同学说,前端页面直接防止用户重复提交表单。没啥毛病,但网络错误会导致重传,很多RPC框架、网关都有自动重试机制,所以重复请求无法避免。问题最终还是归结于如何保证服务接口的幂等性。


如何判断请求是重复的?


插入订单前,先查一下订单表里面有无重复订单?

这可不好,你很难用SQL条件定义什么是“重复订单”


订单的用户、商品、价格一样就是重复订单?

万一这用户就是连续下了俩一模一样订单呢!


所以保证幂等性要做到如下几点!


每个请求须有唯一标识


比如订单支付请求,得包含订单id,一个订单id最多支付一次!


每次处理完请求后,须有记录标识该请求已被处理


在MySQL中记录一个状态字段。比如支付之前记录一条这个订单的支付流水。


每次接收请求判断之前是否处理过


若有一个订单已支付,就已经有了一条支付流水。若重复发送这个请求,则此时先插入支付流水,orderId已存在,唯一键约束生效,报错插入不进去。就不会再重复扣款。


在往db插条记录时,一般不提供主键,而由数据库在插入时自动生成一个主键。这样重复的请求就会导致插入重复数据。

MySQL的主键自带唯一性约束,若在一条INSERT语句提供主键,且该主键值在表中已存在,则该条INSERT会执行失败。因此可利用db的“主键唯一约束”,在插数据时带上主键,以此实现创建订单接口的幂等性。


给订单服务添加一个“orderId生成”的接口,无参,返回值就是一个全局唯一订单号。在用户进入创建订单页面时,前端页面先调用该orderId生成接口得到一个订单号,在用户提交订单的时候,在创建订单的请求中携带该订单号。


该订单号其实就是订单表的主键,如此一来,重复请求中带的都是同一订单号。订单服务在订单表中插入数据的时候,执行的这些重复INSERT语句中的主键,也都是同一个订单号。而数据库的唯一约束可保证,只有一次INSERT执行成功。


实际要结合业务,比如使用Redis,用orderId作为唯一键。只有成功插入这个支付流水,才可执行扣款。


要求是支付一个订单,必须插入一条支付流水,order_id建立一个唯一键。

你在支付一个订单前,先插入一条支付流水,order_id就已经传过去了。就可以写一个标识到Redis中,set order_id payed,当重复请求过来时,先查Redis的order_id对应的value,若为payed说明已支付,就别重复支付了!


然后再重复支付订单时,写尝试插入一条支付流水,db会报唯一键冲突,整个事务回滚即可。

保存一个是否处理过的标识也可以,服务的不同实例可以一起操作Redis。


幂等创建订单的时序图

26.png

如果因为重复订单导致插入订单表失败,订单服务不要把这个错误返回给前端页面。否则,就可能出现用户点击创建订单按钮后,页面提示创建订单失败,而实际上订单却创建成功了。正确的做法是,遇到这种情况,订单服务直接返回订单创建成功即可。

3 怎么解决ABA问题?

3.1 什么是 ABA?

比如订单支付后,卖家要发货,发货完成后要填个快递单号。假设说,卖家填了个666,刚填完,发现填错了,赶紧再修改成888。对订单服务来说,这就是2个更新订单的请求。系统异常时666请求到了,单号更成666,接着888请求到了,单号又更新成888,但是666更新成功的响应丢了,调用方没收到成功响应,自动重试,再次发起666请求,单号又被更新成666了,这数据显然就错了.


时序图

28.png

3.2 解决方案

通用的解决方案

订单主表增加一列version。每次查询订单的时候,版本号要随着订单数据返回给页面。

页面在更新数据的请求中,把这个版本号作为更新请求的参数,带回给订单更新接口。


订单服务在更新数据的时候,需要比较订单的版本号是否和消息中的一致:


不一致 拒绝更新数据

一致 还需要再更新数据的同时,把版本号+1。“比较版本号、更新数据和版本号+1”,这个过程必须在同一个事务里面执行。

UPDATE orders set tracking_number = 666, version = version + 1
  WHERE version = 8;

在这条SQL的WHERE条件中,version的值需要页面在更新的时候通过请求传进来。


通过这个版本号,就可以保证,从我打开这条订单记录开始,一直到我更新这条订单记录成功,这个期间没有其他人修改过这条订单数据。因为,如果有其他人修改过,数据库中的版本号就会改变,那我的更新操作就不会执行成功。我只能重新查询新版本的订单数据,然后再尝试更新。


有了这个版本号,前文的ABA即有两个 case


把运单号更新为666的操作成功了,更新为888的请求带着旧版本号,那就会更新失败,页面提示用户更新888失败

第二种情况,666更新成功后,888带着新的版本号,888更新成功。这时候即使重试的666请求再来,因为它和上一条666请求带着相同的版本号,上一条请求更新成功后,这个版本号已经变了,所以重试请求的更新必然失败

无论哪种情况,数据库中的数据与页面上给用户的反馈都是一致的。这样就可以实现幂等更新并且避免了ABA问题


下图展示case1

31.png

4 总结

  • 对于创建订单服务来说,可以通过预先生成订单号,然后利用数据库中订单号的唯一约束这个特性,避免重复写入订单,实现创建订单服务的幂等性
  • 对于更新订单服务,可以通过一个版本号机制,每次更新数据前校验版本号,更新数据同时自增版本号,这样的方式,来解决ABA问题,确保更新订单服务的幂等性。

两种幂等的实现方法,就可以保证,无论请求是不是重复,订单表中的数据都是正确的。


实现订单幂等的方法,完全可以套用在其他需要实现幂等的服务中,只需要这个服务操作的数据保存在数据库中,并且有一张带有主键的数据表即可


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
NoSQL Java API
分布式系统---幂等性设计
分布式系统---幂等性设计
100 1
|
NoSQL 安全 Redis
Redis 分布式锁实现方案
Redis 分布式锁实现方案
4783 4
|
存储 分布式计算 Hadoop
分布式数据库HBase的常用操作的对应的API编程接口
HBase是一个分布式数据库系统,基于Google的BigTable和Apache Hadoop的HDFS构建。它提供了一个高性能、可扩展的数据库平台,适用于大规模的数据存储和处理。在阿里云开发者社区中,很多开发者都会使用HBase进行数据存储和处理。本文将介绍HBase的常用操作及其对应的API编程接口。
287 0
|
3月前
|
SQL 索引
分布式之接口幂等性
分布式之接口幂等性
46 2
|
3月前
|
消息中间件 存储 C#
分布式事务之最终一致性实现方案
分布式事务之最终一致性实现方案
78 0
|
算法 NoSQL 关系型数据库
分布式系统第三讲:全局唯一ID实现方案
分布式系统第三讲:全局唯一ID实现方案
409 0
|
负载均衡 Dubbo NoSQL
Dubbo分布式服务接口的幂等性防止重复扣款
Dubbo分布式服务接口的幂等性防止重复扣款
210 0
|
6月前
|
XML 安全 Java
【分布式技术专题】「单点登录技术架构」一文带领你好好对接对应的Okta单点登录实现接口服务的实现落地
【分布式技术专题】「单点登录技术架构」一文带领你好好对接对应的Okta单点登录实现接口服务的实现落地
327 0
|
消息中间件 Java 关系型数据库
分布式系统第五讲:分布式事务及实现方案
分布式系统第五讲:分布式事务及实现方案
166 0
分布式接口幂等性、分布式限流(Guava 、nginx和lua限流)
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。

热门文章

最新文章