常见的分布式事务处理方式有:2PC、TCC、异步确保型,2PC的处理方式,在以前的《Spring系列(9)-多数据源和2PC分布式事务》中已经写过,本文针对后二者分享。前端
一、本地消息表(异步确保)
1.一、特色
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操做知足事务特性,而且使用了消息队列来保证 最终一致性。数据库
- 在分布式事务操做的一方完成写业务数据的操做以后向本地消息表发送一个消息,本地事务能保证这个消息必定会被写入本地消息表中。
- 以后将本地消息表中的消息转发到 Kafka 等消息队列中,若是转发成功则将消息从本地消息表中删除,不然继续从新转发。“消息队列” 能够用 “轮询程序” 替代,目的是能够经过不断重试,保障事务最终执行成功。
- 在分布式事务操做的另外一方从消息队列中读取一个消息,并执行消息中的操做。该操做必须都是知足幂等性的,幂等性是指同一个操做不管请求多少次,其结果都相同。
1.二、案例
在一个业务系统中,某个主流程节点触发流转操做时,须要同时执行其余多个系统事务(如:安装一站式App在oa流程流转时,须要在sap系统中翻转状态)。只有当下列全部其余系统的事务都成功完成后,主流程业务才流转成功:小程序
- 调用A系统的接口,完成事务A。
- 调用B系统的接口,完成事务B。
- 调用C系统的接口,完成事务C。
1.三、步骤
- (本地订单表)即主业务系统的业务表,有惟一标识的“业务流水号”。
- (本地消息表)主业务系统流程流转时,本地消息表中分别写入须要执行A系统、B系统、C系统3个不一样事务的3条消息,消息的状态为【处理中】。因为都是在同一本地数据库,可保证事务一致性。
- (事务A)A系统有”处理事务A的接口”,该接口保障幂等性。经过 “轮询程序” 遍历全部【处理中】状态下A事务的消息调接口,事务完成后修改消息状态;或经过 “消息队列”,保障事务最终被成功执行后消费消息。
3.1. 针对该“业务流水号”,若是A系统中未处理,则处理该事务,并更新A系统消息的状态为【已处理】。segmentfault
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条消息,是否状态都为【已处理】。若是都是【已处理】,则更新本地订单表中“业务流水号”状态,流程翻转成功。因为都是在同一本地数据库,可保证事务一致性。
二、微信支付JSAPI(异步确保)
2.一、特色
微信官方提供的操做:app
- (API)统一下订单接口。
- (异步SDK)用户在微信客户端上支付该订单(密码支付/指纹支付/人脸识别/等等)。
- (API)用户异步支付成功后,触发回调接口,通知商户号。
- (API)查询订单状态接口。
在微信支付成功后,一样会同时执行其余多个系统事务,此时的“支付成功”操做就等价于案例一中的“某个主流程节点”。可是有些不一样,因为该“支付成功”操做是由微信官方平台异步触发的,因此可能会回调失败,也须要咱们不断重试,已保障最终全部成功支付的订单都会触发后续事务操做。框架
2.二、案例
微信小程序使用JSAPI方式微信支付,给饭卡充值。在支付成功后会同时执行其余多个系统事务,只有当下列全部其余系统的事务都成功完成后,饭卡充值业务才成功完成:异步
- 在充值系统中,充值的金额增长到饭卡余额中。
- 在帐单系统中,记录充值/消费记录。
- 在通知系统中,推送充值成功的消息(app内通知/短信通知)。
2.三、步骤
- (本地订单表)用户输入金额,充值。后台调【微信统一下单接口】生成微信支付订单,接口返回成功后,在本地订单表中建立订单记录,状态为【支付中】。
- 前端异步回调,微信小程序拉起微信SDK组件,用户开始支付。
- (事务P+本地消息表)提供【微信支付成功回调接口】,该接口支持幂等性-若是查询微信订单状态已支付,且本地消息表中无事务处理消息,则执行。执行操做为,在本地消息表中分别写入须要执行A、B、C3个不一样事务的3条消息,消息的状态为【处理中】。
3.1. 将执行事务P的【微信支付成功回调接口】配置在微信下单的接口中,在异步支付成功后调用该接口。若是未收到成功反馈,微信官方会重发回调方法,可是有重发的次数限制。分布式
3.2. “轮询程序”或“消息队列”,两种方案来处理未成功执行了的事务P。重试调用【微信支付成功回调接口】,直到成功支付了的订单,都能将A、B、C3个不一样事务的3条消息写入本地消息表。
- (事务P 轮询)轮询程序遍历本地订单表中【支付中】的订单,经过【微信订单查询接口】查询订单状态:
4.一、若是订单已支付,则说明【微信支付成功回调接口】未触发,或其余缘由致使的事务异常。自动调用(事务B)中【微信支付成功回调接口】。
4.二、若是订单未支付,且未到取消订单的超时时间,则不处理订单。
4.三、若是订单未支付,且超过取消订单的超时时间,更新订单状态为【已做废】。
- (事务A)充值系统中提供该接口,有幂等性。针对该惟一标识等订单流水号,若是已经在帐户中完成对应充值的余额更新,则不处理,不然更新帐户余额。并更新消息表中对应的消息状态为【已处理】。
- (事务B)帐单系统中提供该接口,有幂等性。针对该惟一标识等订单流水号,若是已经在帐单中已记录该次充值的帐单记录,则不处理,不然记录该次充值的帐单记录。并更新消息表中对应的消息状态为【已处理】。
- (事务C)通知系统中提供该接口,有幂等性。针对该惟一标识等订单流水号,若是已经在通知系统中已发送该次充值的消息,则不处理,不然发送该次充值的消息。并更新消息表中对应的消息状态为【已处理】。
- (本地订单表+本地消息表)经过轮询程序,或经过A/B/C每一个消息的回调通知,触发调用一个一样幂等的方法。该方法判断该“业务流水号”在本地消息表中A、B、C事务的3条消息,是否状态都为【已处理】。若是都是【已处理】,则更新本地订单表中“业务流水号”状态,流程翻转成功。因为都是在同一本地数据库,可保证事务一致性。
2.四、对比
“案例一”中普通的异步确保,和“案例二”中微信支付的异步确保,两套方案的实现上有些差异。
差异体如今,如何将 “主业务流程触发” 和 “将A、B、C事务的3个消息写入消息中间表” 这两步操做做为一个事务,并保证它的原子性。
- 案例一:“主业务流程流转”,和“将A、B、C事务的3个消息写入消息中间表”,都是在同一个数据库下的操做,能够直接使用数据库的事务实现。
- 案例二:“主业务流程触发”实际对应的是“微信支付成功”,这个是由微信支付官方平台提供的异步触发事务,而“将A、B、C事务的3个消息写入消息中间表”则是本地数据库事务。两者自己已是分布式事务处理了。因此引入了 “事务P”,并提供 “执行事务P的幂等性接口”。经过轮询或消息队列来监听本地订单表,经过不断重试该幂等性接口,保证事务的原子性。
三、商品抢购(TCC补偿)
3.一、特色
你本来的一个接口,要改造为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 逻辑都回滚,全部服务都不要执行任何设计的业务逻辑。保证你们要么一块儿成功,要么一块儿失败。
一、Try: 尝试执行业务
- 完成全部业务检查(一致性)
- 预留必须业务资源(准隔离性)
二、Confirm: 确认执行业务
- 真正执行业务
- 不做任何业务检查
- 只使用Try阶段预留的业务资源
- Confirm操做知足幂等性
三、Cancel: 取消执行业务
- 释放Try阶段预留的业务资源
- Cancel操做知足幂等性
3.二、案例
一个商品抢购的业务,商品库存100份,单价6元/份,多人抢购。A用户帐户余额50元,抢购了2份该商品。只有当下列系统的事务都执行成功后才会抢购完成:
- 商品系统:检查当前库存是否足够,如足够,则减去2份商品。
- 用户系统:检查余额是否足够,如足够,则扣除相应金额。
- 订单系统:检查是否知足建立订单条件,如知足,则建立订单。
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个幂等的接口,接口均可经过轮训或消息队列等方式重试。
- (库存)将冻结数量还给库存数量。
- (用户)将冻结金额还给帐户余额。
- (订单)删除或做废当前订单。