幂等是分布式系统中保证数据一致性和安全性的重要保障之一,尤其是在金融、支付领域,其作为资损防控的硬性指标体现在系统架构设计中。今天我们就来浅谈一下幂等相关的设计。
幂等的定义幂等( 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 流水,返回已经受理的关键业务信息。调用方:对服务方返回的幂等内容做校验,确保与预期一致。总结以上规则是借鉴历史项目和互联网经验总结而成,主要侧重于幂等设计的原则,幂等的落地方案有很多,比如幂等表、乐观锁、悲观锁等,这里就不赘述。
标签: