分布式高并发系统如何保证对外接口的幂等性?

分布式高并发系统如何保证对外接口的幂等性?

幂等意味着,对于同一个交易的多次处理,结果不变。怎么讲呢?

假如客户账户Account1现在有100元,执行一次交易(Order01)扣款10元,现在有就变成了90元,在分布式情况下,假如网络超时失败之类的情况下,就会导致可能客户端拿到了TimeoutException,但是实际上Server端执行成功了,这时客户端可能会发起重试,或者用MQ处理的话,MQ会重试。如果再执行一次就扣款10元,就变成了80元,显然这是不对的。况且如果重试操作发生了多次,余额被扣成了负数,客户会疯掉。理想的状态就是,多次的重复处理,结果应该不变,还是90元。假设处理订单的服务方法是f,那么就是:

Step1:f(100, Order01) => 90
Step2:f(90, Order01) => 90
Step3:f(90, Order01) => 90

方法f怎么实现呢?伪代码:

function f(Account, Order){
  Account = Account - Order.amount;
  // update user_account set account = account - orderAmount where userid = xxx;
  //  90  = 100    - 10; 
}

这段代码实现很简单,就是直接把账户减掉订单金额。这个逻辑明显有问题,在上面的Step2步骤的时候,会导致结果是80,而不是我们期望的不变,还是90元。

一个改进办法就是我们可以参考乐观锁:每次先拿到当前的某个版本号标志或者价格,然后修改的时候把这个标识作为条件,就可以在余额已经变化的情况下,不操作了(核心原理就是通过某个条件,判断记录是不是已经变化了):

currentAccount = Account;// 100

function f(currentAccount, Account, Order){
  Account = if(Account == currentAccount) Account - Order.amount;
  // update user_account set account = account - orderAmount 
  //                   where userid = xxx and account = currentAccount; // 100
  //  if(account = 90, currentAccount = 100, 那么这个扣减的操作就不执行,金额不变,实现幂等)
}

这样,只要在一个事务操作里,多次操作是没有关系的,发现金额变了,就不会再变了。多加的幂等判断条件是时间戳,版本号字段,等等都可以,只要是能代表字段被修改过就成。

上面的处理,还有什么问题吗?答案是,如果在上一个订单处理了一段时间以后又有别的订单处理or并发处理,或者是原有订单数据被人工的修订过,那么可能幂等条件被破坏掉了。

继续用上面的例子,假如订单Order01成功以后,订单Order02这时候增加了10元,那么账户又变成100元,这时候要是Order01再来一次,显然要是按照上面说的按100元的条件对比,还是会执行。

Step1:f(100, Order01) => 90
Step2:f(90, Order01) => 90
Step3:f(90, Order02) => 100 // 隔了一段时间做了这个操作
Step4:f(100, Order01) => 90 //这里就不对了,应该不变

怎么办呢?

一个简单的办法就是,把成功执行过的订单id都记录下来,或者把订单处理的事务里,给订单记上一个成功状态,这个订单状态的操作一定要和账户操作在一个同步的事务里。每次操作的时候用这个状态作为条件,相当于一个悲观锁,就不会重复了。

那么有没有跟简单的办法呢?如果我们假设订单不多,或者可能会重复的数据集中在一定时间范围内,那么可以用内存的办法去重,更简单高效:

例如,我们把新进操作成功的OrderID都直接丢到RoaringBitmap里,假如一天以内的数据是热数据有10-20万,这10-20万个都放到这种压缩的bitmap问题不大,占用内存很小(2-10M),使用也简单,直接检查是不是某个id就可,如果存在就不做任何操作。完全不碰数据库,所以没有io操作,内存的检查操作代价在微秒级。系统每次重启初始化的时候,可以把上一批天的id预先拉出来加载到内存即可。如果获取一天以上的数据,可以先从DB加载,然后放到bitmap,同时可以考虑一下,系统的业务处理流程是不是有问题,或者处了故障,才会导致1天以上数据重复处理,从设计的源头解决问题。。。

可以看到,这种设计比前面的方式复杂的多,但是如果在你的场景里可用,也比其他方法高效的多。高效的方法,往往跟上下文,具体的场景有关系,需要深刻分析问题来探索。

总结一下:幂等就是让多次重复操作的结果跟一次操作的结果是一样的。

可以采用的办法:

  1. 乐观锁,加上操作时的状态判断,如果状态变了就不操作;
  2. 悲观锁,每次操作的时候锁住资源,使用一个显式的状态去表示操作状态;
  3. 使用RoaringBitmap做一定时间范围的热数据内的操作记录做内存bitmap去重,从而实现幂等,我们线上的每日几亿订单的交易订单处理,处理环节大部分用到了MQ,为了防止MQ消费数据重复(MQ本身重复、下游系统启动拉取重复、失败重试带来的重复、补偿逻辑导致的重复),都默认封装了使用RoaringBitmap来去重的机制,非常还用,谁用谁知道。

附RoaringBitmap的github地址:

RoaringBitmap/RoaringBitmap​github.com
 
原文地址:https://www.cnblogs.com/liran123/p/14540656.html