浅谈幂等,大家都看明白了吗?

网站建设1年前发布 whoami
156 00

前言

幂等是分布式系统中保证数据一致性和安全性的重要保障之一,尤其是在金融、支付领域,其作为资损防控的硬性指标体现在系统架构设计中。今天我们就来浅谈一下幂等相关的设计。

幂等的定义

幂等( idempotent、idempotence )的概念来源于数学,并被广泛应用于计算机科学。在数学中,其语意是 f ( x ) = f ( f ( x )),比如求取绝对值,abs ( x ) = abs ( abs ( x )),就是幂等的。

在计算机科学中,幂等即相同的请求调用一次和调用多次,服务端处理的的结果相同,并且最多受理一次。

幂等的重要性

我们就拿支付公司的资金调拨举个例子。一般的,第三方支付公司需要借助清算公司(如网联)提供的支付通道进行备付金账户资金调拨,以保证资金池充足可用。当第三方支付公司发起资金调拨请求时,如果清算公司的返回结果丢失,这时,支付公司是否可以重试?如果重试,是否会发生资金的重复调拨?

浅谈幂等,大家都看明白了吗?

互联网公司的应用间存在物理边界,请求和响应信息会通过网络进行传递。我们说远程调用的结果会有三个状态:成功,失败,未知。前两者都是明确的状态,而未知具有不确定性,一般都是由网络超时、丢包引起的。如上例中,如果出现了超时,其实有两种方案,我们可以建立查询补偿机制,来研判是否要重新发起资金调拨。或者,清算公司做好幂等控制,支付公司可以无脑重试,既可以保证资金调拨业务的正常,又能保证不会发生多次调拨。

在架构设计中,幂等的应用面非常广泛,比如 MQ 规避重复消费、表单规避重复提交等。

幂等设计

幂等两大要素

幂等包含两大要素,幂等标记和关键请求参数。

幂等号:它对应服务端的唯一约束,在设计上,它一般由上游的幂等单号和来源组成。服务端的接口文档中,需要明确指出幂等号的信息组成,它的作用是对请求信息进行身份标识,相同幂等号的请求将被服务端识别为同一请求。

关键请求信息:接收的核心业务信息,常见的如收款账户、打款账户,打款金额、币种、商品数量等等。相同的请求中,调用方需要保证关键请求信息不变,一旦信息发生变动,则需要替换幂等号。

幂等原则

调用方必须保证幂等号的唯一性、不变性

说明

调用方需要保证幂等号不重复,且对同一业务单据的同一次操作,无论请求多少次,都要保证幂等号不变。

反例

幂等号重复,原因基本如下

  • sequence cycle 问题,未评估好业务量同 sequence 增长速度,导致幂等号重复。
  • sequence 步长、分段设置问题,导致跨区域/单元/库/表幂等号重复;

幂等号变化,原因基本如下

  • 事务中生成幂等号,并发起远程调用,调用超时本地事务回滚,第二次请求又会生成新的幂等号。

调用方必须保证关键业务请求参数的不变性

说明

当服务端没有返回结果时,调用方关键业务请求参数不允许变更。

反例

初次请求,由于网络异常导致 timeout 调用方没有拿到结果,而服务端受理成功。客户端修改单据金额,请求信息发生变化,调用方与服务端处理出错。

浅谈幂等,大家都看明白了吗?

img

调用方禁止幂等号纯内存拼接,不进行持久化

说明

幂等号不持久化,对于异步回执处理,上下游数据稽核带来困难,所以幂等号持久化是一个基本要求。

反例

RPC 调用,调用方的幂等号,是内存中根据业务映射拼接得来,不做持久化。

//内存中拼接幂等号
request.setRequestId(BizTypeEnum.getPrefix(×xxDO.getBizType()) + xxxDO.getId()):

调用方幂等号生成事务内禁止包含 RPC

反例
transactionTemplate.execute (status ->
    //生成流水号 xxx 
    SerialDO serialDO = buildSerialDO();
    //播入 aaa 表
    serialDAO.insert(serialDO);
    someDAO.update (someDO) ;
    // dubbo 调用 rpc,流水号 xxxId 作为幂等号
    invokeRpc(request);
    return true,
));
正例
  • RPC 放在事务外面
transactionTemplate.execute (status ->
    //生成流水号 xxx 
    SerialDO serialDO =  buildSerialDO();
    //播入 aaa 表
    serialDAO.insert(serialDO);
    someDAO.update (someDO) ;
    return true;
));
// dubbo 调用 rpc,流水号 xxxId 作为幂等号
invokeRpc(request);
  • 使用事务同步器:如果事务在外层开启,为了不破坏代码结构,使用事务同步器,事务提交后发起 RPC 调用,调用异常后应用需要做恢复。
/**
* 外层已开启事务
*/
public static void execute (){
  //更新单据状态
  Runnable runnable = () -> {
    response = dubboService.call(request);
  };
 register(runnable);
}
 

public static void register (Runnable runnable) {
  if (TransactionSynchronizationManager.isActualTrangactionActive()) {
    TransactionSynchronizationManager.registersynchronization(
      new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit () {
          runnable.run(); 
        } 
      }
    ); 
  } else {
    LOGGER.debug( "No active transaction.");
    runnable.run();
  }
}
  • 业务自研组件:事务中插入本地任务,统一恢复执行。

服务端不能单纯依赖查询做幂等

说明

分布式下并发场景,并不能单纯的依赖查询做到插入 幂等。常见唯一性保障方式:

  • DB 约束:对插入流水的幂等号建 DB 唯一索引约束
  • 分布式锁:如 redis、 zookeeper 等。若持久层在 DB,不推存使用(依赖外部存储做幂等控制,与 DB 的强一致性无法保证),涉及资金等强一致性场景不推荐。
反例

RPC 调用超时,本地事务回滚。下次重试,会生成新的幂等号,导致资损。

服务端必须保证受理结果一致性

说明

针对相同请求,不论调用方请求多少次,服务端仅受理一次,且受理结果相同。

反例

售中退款的场景中,第一次服务端正常受理调用方请求,但调用方因为超时丢弃响应;当第二次调用方重试,服务端发现退款金额不足,返回受理失败,导致故障。

//1、基本校验
//2、悲观锁内,可退款金额判断;
Assert.isTrue(refundable(xxx), "cannot refund");

//3、逻辑处理
try {
 process(xxx);
} catch (Exception e) {
   //幂等判断处理
}

调用方收到服务端幂等结果后,比对关键业务参数

说明

客户端收到服务端结果后,本着不信任的原则,针对关键业务请求参数如账户、 金额同服务端受理内容对比。

反例

服务端做幂等判断时,只看幂等号,虽然第二次请求幂等号不变,但是金额又可能被篡改,如果服务端直接返回成功,将导致资金损失。

正例
  • 服务端:根据幂等号查询 DB 流水,返回已经受理的关键业务信息。
  • 调用方:对服务方返回的幂等内容做校验,确保与预期一致。

总结

以上规则是借鉴历史项目和互联网经验总结而成,主要侧重于幂等设计的原则,幂等的落地方案有很多,比如幂等表、乐观锁、悲观锁等,这里就不赘述。

© 版权声明

相关文章