幂等性问题广泛出现在电商平台的购买商品和支付业务上
首先,我们需要明确接口幂等性的含义,为什么需要保证接口的幂等性?
然后,如何才能实现接口的幂等性,在分布式微服务架构下使用什么方案来实现呢?
Github issues:https://github.com/littlejoyo/Blog/issues/
1、什么是幂等性?
含义:一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
接口的幂等性:就是接口重复执行和一次执行的产生逻辑和结果都是一致的。
2、什么情况下需要保证接口的幂等性?
从数据库操作的增删改查来逐个说明情况
2.1 查询操作
查询对于结果是不会有改变的,查询一次和查询多次,在数据不变的情况下,查询结果是一样的。
select是天然的幂等操作,无需再去实现幂等性操作。
2.2 删除操作
删除一次和多次删除都是把数据删除。
注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个
在不考虑返回结果的情况下,删除操作也是具有幂等性的。
2.3 更新操作
更新操作需要区分情况,如果是更新为固定值的话,每次执行的结果都是一致的
如果是增量修改或者是减法操作,这种场景下是需要保证幂等性的
情况一:update为固定值
把表中id为XXX的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的
UPDATE tab1 SET col1=1 WHERE col2=2
无论执行成功多少次状态都是一致的,因此也是幂等操作。
情况二:递增或者递减
把表中id为XXX的记录的A字段值增加1,这种操作就不是幂等的
UPDATE tab1 SET col1=col1+1 WHERE col2=2
每次执行的结果都会发生变化,这种不是幂等的。
2.4 新增操作
增加在重复提交的场景下会出现幂等性问题
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了
用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条
这就是没有保证接口的幂等性造成的结果,作为消费者你当然不可能满意
3.保证幂等性的方案
3.1 token机制
1.原理上通过session token来实现的(也可以通过redis来实现)。
2.当客户端请求页面时,服务器会生成一个随机数Token,并且将Token放置到session当中(或者redis中),然后将Token发给客户端(一般通过构造hidden表单)。
3.下次客户端提交请求时,Token会随着表单一起提交到服务器端。
4.服务器端第一次验证相同过后,会将session中的Token值更新下(如果是redis要更新键值),若用户重复提交,第二次的验证判断将失败
5.因为用户提交的表单中的Token没变,但服务器端session中(或redis)的Token已经改变了。
6.如果想要实现分布式服务的接口幂等性,需要使用redis来存储token值,基本原理一致,因为session仅存在单机服务器上,跨域共享要研究分布式session的实现。
3.1.1 先删除token还是后删除token
后删除token:
如果进行业务处理成功后,删除redis中的token却失败了
下一次请求还是能对的上token值,再一次发生了接口的请求处理
这样就导致了重复请求并处理的结果,因为token没有被删除
先删除token:
如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果
然后进行尝试重新请求,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。
先删除token可以保证不会因为重复请求,业务数据出现问题。
如果出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了,而不会导致接口重复请求得到不同的结果。
3.1.2 token机制的缺点
增加了token验证的业务处理,每次请求都会额外增加进行验证的环节
其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。
但是为了确保类似支付问题和订单问题的幂等性操作,这是值得的。
3.2 唯一ID
调用接口的时候,内部生成一个唯一id作为调用成功的标志
然后将唯一id存入redis的set集合里(去重)
每次请求都会去redis中查询是否存在id,如果存在说明已经是重复请求,不存在是第一次请求,从而保证了接口的幂等性
3.3 乐观锁机制
乐观锁机制主要应用在更新的场景中,在进行更新操作前先获取version版本号,然后操作的时候也要带上version号。
例如:
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
第一次操作库存时,得到version为1,调用库存服务version变成了2;
但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的version还是1,再执行上面的sql语句时,就不会执行;
因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要应用于读多写少的情况下。
3.4 唯一主键
利用了数据库的唯一约束的特性,解决了在insert场景时幂等问题。
要点:唯一索引或唯一组合索引来防止新增数据存在脏数据。
防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,给资金账户表中的用户ID加唯一索引,保证一个用户只能有一条资金账户数据
当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据已经存在了,返回存在的结果即可,避免了出现新增多条数据的情况;
3.5 建立防重复表
例如使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。
这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。
这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
3.6 状态机幂等性
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更
一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助
设置固定的状态类型,只有同时满足该状态类型下的多个状态,才能执行
比如存在两张订单:
A:订单
待付款
待发货 contain 待发货
已发货 全部是已发货 已收货 退款 退货
已收货 全部已收货
退款中
退款成功
退款失败
退货中
退货成功
退货失败
已取消
- B:订单(不存入库状态)
待付款
待发货(未确认、已确认)
已发货
已收货
退款中
退款成功
退款失败
退货中
退货成功
退货失败
已取消
4.总结
接口的幂等性就是保证一次请求和重复请求后,获得的返回结果是一样的
实现幂等性的方案有多种,具体选择要根据业务进行分析
应用在分布式的架构下,token机制是目前使用较为广泛的其中一种方案
幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好。
微信公众号
扫一扫关注Joyo说公众号,共同学习和研究开发技术。