常见的分布式事务处理方式有:2PC、TCC、异步确保型,2PC的处理方式,在之前的《Spring系列(9)-多数据源和2PC分布式事务》中已经写过,本文针对后两者分享。
1、本地消息表(异步确保)
1.1、特点
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证 最终一致性。
- 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
- 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。“消息队列” 可以用 “轮询程序” 替代,目的是可以通过不断重试,保障事务最终执行成功。
- 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。该操作必须都是满足幂等性的,幂等性是指同一个操作无论请求多少次,其结果都相同。
1.2、案例
在一个业务系统中,某个主流程节点触发流转操作时,需要同时执行其他多个系统事务(如:安装一站式App在oa流程流转时,需要在sap系统中翻转状态)。只有当下列所有其他系统的事务都成功完成后,主流程业务才流转成功:
- 调用A系统的接口,完成事务A。
- 调用B系统的接口,完成事务B。
- 调用C系统的接口,完成事务C。
1.3、步骤
- (本地订单表)即主业务系统的业务表,有唯一标识的“业务流水号”。
- (本地消息表)主业务系统流程流转时,本地消息表中分别写入需要执行A系统、B系统、C系统3个不同事务的3条消息,消息的状态为【处理中】。由于都是在同一本地数据库,可保证事务一致性。
- (事务A)A系统有”处理事务A的接口”,该接口保障幂等性。通过 “轮询程序” 遍历所有【处理中】状态下A事务的消息调接口,事务完成后修改消息状态;或通过 “消息队列”,保障事务最终被成功执行后消费消息。
3.1. 针对该“业务流水号”,如果A系统中未处理,则处理该事务,并更新A系统消息的状态为【已处理】。
3.2. 针对该“业务流水号”,如果A系统中已处理,则不处理该事务。
3.3. 假设:A系统是某sap系统,需要将某”业务流水号“对应的物料数量“加一”,当前物料数量基础数量是5。”处理事务A的接口”即是sap系统开发并提供的接口,该sap接口判断,如果并未处理该“业务流水号”的事务,则将物料基础数量更新为6;如果已处理过该“业务流水号”的事务,则物料基础数量不变。因为“幂等性”,不会导致每次调接口都会往上“加一”。
- (事务B)B系统和A系统处理机制类似。
- (事务C)C系统和A系统处理机制类似。
- (本地订单表+本地消息表)通过轮询程序,或通过A/B/C每个消息的回调通知,触发调用一个同样幂等的方法。该方法判断该“业务流水号”在本地消息表中A、B、C事务的3条消息,是否状态都为【已处理】。如果都是【已处理】,则更新本地订单表中“业务流水号”状态,流程翻转成功。由于都是在同一本地数据库,可保证事务一致性。
2、微信支付JSAPI(异步确保)
2.1、特点
微信官方提供的操作:
- (API)统一下订单接口。
- (异步SDK)用户在微信客户端上支付该订单(密码支付/指纹支付/人脸识别/等等)。
- (API)用户异步支付成功后,触发回调接口,通知商户号。
- (API)查询订单状态接口。
在微信支付成功后,同样会同时执行其他多个系统事务,此时的“支付成功”操作就等价于案例一中的“某个主流程节点”。但是有些不同,由于该“支付成功”操作是由微信官方平台异步触发的,所以可能会回调失败,也需要我们不断重试,已保障最终所有成功支付的订单都会触发后续事务操作。
2.2、案例
微信小程序使用JSAPI方式微信支付,给饭卡充值。在支付成功后会同时执行其他多个系统事务,只有当下列所有其他系统的事务都成功完成后,饭卡充值业务才成功完成:
- 在充值系统中,充值的金额增加到饭卡余额中。
- 在账单系统中,记录充值/消费记录。
- 在通知系统中,推送充值成功的消息(app内通知/短信通知)。
2.3、步骤
- (本地订单表)用户输入金额,充值。后台调【微信统一下单接口】生成微信支付订单,接口返回成功后,在本地订单表中创建订单记录,状态为【支付中】。
- 前端异步回调,微信小程序拉起微信SDK组件,用户开始支付。
- (事务P+本地消息表)提供【微信支付成功回调接口】,该接口支持幂等性-如果查询微信订单状态已支付,且本地消息表中无事务处理消息,则执行。执行操作为,在本地消息表中分别写入需要执行A、B、C3个不同事务的3条消息,消息的状态为【处理中】。
3.1. 将执行事务P的【微信支付成功回调接口】配置在微信下单的接口中,在异步支付成功后调用该接口。如果未收到成功反馈,微信官方会重发回调方法,但是有重发的次数限制。
3.2. “轮询程序”或“消息队列”,两种方案来处理未成功执行了的事务P。重试调用【微信支付成功回调接口】,直到成功支付了的订单,都能将A、B、C3个不同事务的3条消息写入本地消息表。
- (事务P 轮询)轮询程序遍历本地订单表中【支付中】的订单,通过【微信订单查询接口】查询订单状态:
4.1、如果订单已支付,则说明【微信支付成功回调接口】未触发,或其他原因导致的事务异常。自动调用(事务B)中【微信支付成功回调接口】。
4.2、如果订单未支付,且未到取消订单的超时时间,则不处理订单。
4.3、如果订单未支付,且超过取消订单的超时时间,更新订单状态为【已作废】。 - (事务A)充值系统中提供该接口,有幂等性。针对该唯一标识等订单流水号,如果已经在账户中完成对应充值的余额更新,则不处理,否则更新账户余额。并更新消息表中对应的消息状态为【已处理】。
- (事务B)账单系统中提供该接口,有幂等性。针对该唯一标识等订单流水号,如果已经在账单中已记录该次充值的账单记录,则不处理,否则记录该次充值的账单记录。并更新消息表中对应的消息状态为【已处理】。
- (事务C)通知系统中提供该接口,有幂等性。针对该唯一标识等订单流水号,如果已经在通知系统中已发送该次充值的消息,则不处理,否则发送该次充值的消息。并更新消息表中对应的消息状态为【已处理】。
- (本地订单表+本地消息表)通过轮询程序,或通过A/B/C每个消息的回调通知,触发调用一个同样幂等的方法。该方法判断该“业务流水号”在本地消息表中A、B、C事务的3条消息,是否状态都为【已处理】。如果都是【已处理】,则更新本地订单表中“业务流水号”状态,流程翻转成功。由于都是在同一本地数据库,可保证事务一致性。
2.4、对比
“案例一”中普通的异步确保,和“案例二”中微信支付的异步确保,两套方案的实现上有些差别。
差别体现在,如何将 “主业务流程触发” 和 “将A、B、C事务的3个消息写入消息中间表” 这两步操作作为一个事务,并保证它的原子性。
- 案例一:“主业务流程流转”,和“将A、B、C事务的3个消息写入消息中间表”,都是在同一个数据库下的操作,可以直接使用数据库的事务实现。
- 案例二:“主业务流程触发”实际对应的是“微信支付成功”,这个是由微信支付官方平台提供的异步触发事务,而“将A、B、C事务的3个消息写入消息中间表”则是本地数据库事务。二者本身已经是分布式事务处理了。所以引入了 “事务P”,并提供 “执行事务P的幂等性接口”。通过轮询或消息队列来监听本地订单表,通过不断重试该幂等性接口,保证事务的原子性。
3、商品抢购(TCC补偿)
3.1、特点
你原本的一个接口,要改造为3个逻辑,Try-Confirm-Cancel:
- 先是服务调用链路依次执行 Try 逻辑。
- 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
- 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。
这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时:
- 某个服务的数据库宕机了。
- 某个服务自己挂了。
- 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。
- 某些资源不足了,比如说库存不够这些。
先来 Try 一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。
如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、MQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。
接着,再执行各个服务的 Confirm 逻辑,基本上 Confirm 就可以很大概率保证一个分布式事务的完成了。
那如果 Try 阶段某个服务就失败了,比如说底层的数据库挂了,或者 Redis 挂了,等等。
此时就自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。
1、Try: 尝试执行业务
- 完成所有业务检查(一致性)
- 预留必须业务资源(准隔离性)
2、Confirm: 确认执行业务
- 真正执行业务
- 不作任何业务检查
- 只使用Try阶段预留的业务资源
- Confirm操作满足幂等性
3、Cancel: 取消执行业务
- 释放Try阶段预留的业务资源
- Cancel操作满足幂等性
3.2、案例
一个商品抢购的业务,商品库存100份,单价6元/份,多人抢购。A用户账户余额50元,抢购了2份该商品。只有当下列系统的事务都执行成功后才会抢购完成:
- 商品系统:检查当前库存是否足够,如足够,则减去2份商品。
- 用户系统:检查余额是否足够,如足够,则扣除相应金额。
- 订单系统:检查是否满足创建订单条件,如满足,则创建订单。
3.3、步骤
3.3.1. Try
- (事务A-商品)用户A抢购2份。检查库存数量是否大于2,如果库存充足则继续进行。更新库存表中库存数量为98,冻结数量为2。
- (事务B-用户)用户A账户余额50元,购买商品需2元。检查余额是否大于2元,如果余额充足则继续进行。更新余额为48元,冻结2元。
- (事务C-订单)用户A购买商品需创建订单。检查是否满足创建订单条件,如果可以则继续进行。创建订单,但该订单状态为【未生效】。
3.3.2. Confirm
在事务A、B、C都执行成功后,分别调用3个幂等的接口,接口都可通过轮训或消息队列等方式重试。
- (商品)更新冻结数量2为0。
- (用户)更新冻结2元为0.
- (订单)更新订单状态为【有效】。
3.3.3. Cancel:
在Try一直执行失败后,执行Cancel。分别调用3个幂等的接口,接口都可通过轮训或消息队列等方式重试。
- (库存)将冻结数量还给库存数量。
- (用户)将冻结金额还给账户余额。
- (订单)删除或作废当前订单。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。