防重幂等
前言:
在分布式系统下,服务之间相互调用,必然会存在调用失败并且进行重试的情况,在某些情况下就需要做好防重幂等。
防重和幂等是什么?
防重:避免产生重复数据
幂等:除了避免产生重复数据之外,还要求每次请求都返回一样的结果
什么情况会导致重复?
发送方发送相同的请求到服务端。
- 前端多次发送相同的请求到后端
- 超时重发导致的重复
- MQ异常导致的重复消费
如何防重?
-
insert之前先select,通常情况下有效,但是在高并发情况下,也会导致重复
-
建立唯一索引,数据库兜底,防止重复添加
-
某些业务表在特定的场景下才不允许重复,不能直接建立唯一键,就可以增加一张防重表(为此类业务),将此类数据在同一事务下先insert进防重表成功,在insert业务表,假如insert进防重表失败,证明此类数据重复,就不用再处理业务表了
-
加分布式锁(针对单据来锁):需要合理设置过期时间。不能太短,导致业务没有处理完,锁失效,防重失败;也不能不设置过期时间,解锁异常导致锁一直被占,阻塞后续处理。
什么情况要做幂等?
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
例如:
- 比如用户对一笔订单发起付款,因为网络问题没有返回结果,就多次点击付款按钮,此时只能发起一笔真实的交易,生成一条交易记录。
- 分布式系统中,因为接口超时,导致的重试,第一次请求接口超时,没有获取到返回结果(有可能已经成功了),第二次重试,接收方不能直接返回失败,要根据第一次处理的结果进行返回。
怎么解决?
- 新增数据类接口,通过防重解决。
- 更新类接口,比如更新库存,更改状态等,通过状态,加乐观锁解决。
根据状态判断
很多业务是有状态的,比如一个订单表。有下单0、支付中1、已支付2、取消支付3等状态,
假如id=123的订单状态是0,现在要变成支付中状态。
update order set status=1 where id=123 and status=0;
第一次请求时,该订单的状态可以正常更新,sql执行结果的影响行数是1,订单状态变成了1。后面有相同的请求过来,再执行相同的sql时,由于订单状态变成了1,再用status=0作为条件,最终sql执行结果的影响行数是0,即不会真正的更新数据。但为了保证接口幂等性,接口也需要直接返回成功。
加乐观锁,在表中增加一个version字段。
在更新数据之前先查询一下数据:
select id,amount,version from user id=123;
如果数据存在,假设查到的version等于1,再使用id和version字段作为查询条件更新数据:
update user set amount=amount+100,version=version+1 where id=123 and version=1;
更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。
由于第一次请求version等于1是可以成功的,操作成功后version变成2了。这时如果并发的请求过来,再执行相同的sql:
update user set amount=amount+100,version=version+1 where id=123 and version=1;
该update操作不会真正更新数据,最终sql的执行结果影响行数是0,因为version已经变成2了,为了保证接口幂等性,接口可以直接返回成功,因为version值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。
总结
- 网络延迟问题:先发的不一定先到
- 数据库操作延迟:先到的不一定先执行完
- 不能依赖上游或下游去做防重幂等,自己本身也要把控好
- 对于外部接口,没有明确返回可重试状态的,不要轻易重试