背景:在微服务、rpc架构中,我们往往会面临如下的类似场景
用户使用优惠券购买某商品,我们作为支付系统,需要和多个业务模块进行数据交互,假设我们作为支付中心,用户下单需要通知A、B、C三个系统,如图:
|-------- 优惠券系统 A
|
|
|
下单 ------->|-------- 库存系统 B
|
|
|
|-------- 余额系统 C如果我们收到下单请求,直接向A、B、C三个模块分别发送一个请求,如果A、B均成功,但是C 模块处理失败了,就会造成优惠券使用了,库存减一了,但是没有有效订单,那么如何处理呢?
目前比较通用的方案就是利用 XA 分布式事务协议、MQ、TCC。
XA 协议包含 两阶段提交、三阶段提交。
1、两阶段提交
假设本次购买了一台电脑,使用一张优惠券,使用的是用户的账户余额进行购买。第一阶段:
引入一个协调者,对子系统下发请求。
A、B、C 接收后分别开启事务执行业务逻辑,并将事务开启结果回传协调者
具体各个系统的SQL表现如下:
A:
//开启事务,后面的DB名可选,假设100001为订单ID,确保唯一
xa start '100001','databaseA';
//执行业务逻辑
insert into tableA set coupon_id=1001, status=1, use_time = 1591774031;
//将事务置于IDLE状态,表示事务内的SQL操作完成
xa end '100001','databaseA';
//实现事务提交的准备工作,事务状态置于PREPARED状态。事务如果无法完成提交前的准备操作,该语句会执行失败。
xa prepare '100001','databaseA';
B:
xa start '100001','databaseB';
update tableB set num = num - 1 where good_id = 1001 and num > 0;
xa end '100001','databaseB';
xa prepare '100001','databaseB';
C:
xa start '100001','databaseC';
update tableC set surplus = surplus - 999000 where user_id = 1001 and surplus >= 999000;
xa end '100001','databaseB';
xa prepare '100001','databaseB';
当以上三个系统均执行成功后,将XID(事务ID)返回给协调者。
协调者确认三个系统均正常后,进入第二阶段。
第二阶段:
如果第一阶段成功则进行事务的提交
各个系统的SQL表现如下:
A:
//查询处于prepare阶段的事务总数是否符合预期
xa recover;
//符合预期进行提交
xa commit '100001','databaseA';
B:
xa recover;
xa commit '100001','databaseB';
C:
xa recover;
xa commit '100001','databaseC';
如果第一阶段失败则进行事务的回滚
各个系统的SQL表现如下:
A:
xa rollback '100001','databaseA';
B:
xa rollback '100001','databaseB';
C:
xa rollback '100001','databaseC';但仍存在如下问题:
1、如果事务开启后,协调者挂掉,一直没有提交事务的请求,则 A、B、C 系统无法接收指令提交或回滚事务(两阶段提交可以为协调者设置超时时间限制来解决)。
2、为保证数据一致性,A、B、C 系统分别需要开启自己的事务,且提交时间以来协调者发送指令,这种方式较为占用数据库资源。
3、在第二阶段,如果A、B事务提交成功,C事务提交失败,会造成数据不一致问题。
2、三阶段提交
和两阶段提交大致相同,有区别的地方为:
1、在协调者、参与者中都引入了超时机制,当协调者故障, A、B、C 事务迟迟接收不到提交指令,超过一定时间范围后自动 commit 或 rollback。
2、将两阶段提交中的第一阶段拆分为两步:询问、锁定资源。
3、三个步骤分别为:CanCommit、PreCommit、DoCommit
可能存在的问题:
1、PreCommit阶段完成后,协调者发出 rollback 的请求,但只有一个参与者收到了并执行了回滚,另外两个参与者未收到,超时自动 commit 了,此时会出现数据不一致的情况。
3、MQ消息队列
比如用户下单后,支付系统只处理自己的业务逻辑,处理完成后,将数据推送到MQ队列中,由MQ分别推送给各个系统去消费(一般MQ队列推送到各个系统的时候都会失败重试)。如果A、B、C 各个系统接收到MQ的数据之后处理各自的业务逻辑即可。例如百度的nmq
但仍然存在以下问题:
1、支付系统处理成功后,A、B、C 各个系统在处理时发现超库存了或者其它情况, 一直无法正常结束整个流程,此时就需要人为介入,或者引入一些报警机制,同样面临两阶段提交中的 问题3。
4、TCC
TCC主要是针对两阶段提交做的一个优化,大致分为三个步骤:
1、Try阶段:检查资源并锁定资源
2、Confirm阶段:当发起者收到 try 的成功返回,即进行 confirm 逻辑
3、Cancel阶段:如果try阶段发生了异常,执行该动作。
举例:
假设本次购买了一台电脑,使用一张优惠券,使用的是用户的账户余额进行购买。
我们分别为每个系统设置一个锁定记录表,如下:
A: id trade_id coupon_id status
B: id trade_id good_id num status
C: id trade_id user_id cost status1、Try
下单系统分别向 A、B、C 三个系统下发请求
假设本次购买了一台电脑,使用一张优惠券,使用的是用户的账户余额进行购买
各系统受到请求后往对应的表内添加锁定记录
status=0代表默认不处理,status=1代表锁定中,status=2代表已处理并解锁
A:
inser into tmpA set trade_id=100001, coupon_id=1001, status=1;
B:
inser into tmpB set trade_id=100001, good_id=1001, num=1, status=1;
C:
inser into tmpC set trade_id=100001, user_id=1001, cost=999900, status=1;
边界:
1、假设A、B、C 子系统只要有一个出错,即执行回滚操作,即Cancel
2、在另外一个进程下单时,需要考虑已被锁定的数据。2、Confirm
只有当Try阶段均执行成功了,才会执行此部分
各个系统的sql表现如下:
A:
update tmpA set status = 2 where trade_id = 100001;
//同时update优惠券使用表,减少真实的数据
B:
update tmpA set status = 2 where trade_id = 100001; //同时update库存表,减少真实的库存
C:
update tmpA set status = 2 where trade_id = 100001;
//同时update余额表,扣减真实的余额
边界:
1、如果A系统执行成功了,B也执行成功了,C执行失败了,如何处理?
思路:无限重试,确保成功3、Cancel
系统某一步出错,需要进行回滚,各个系统的sql表现如下
A:
update tmpA set status = 0 where trade_id = 100001;
B:
update tmpB set status = 0 where trade_id = 100001;
C:
update tmpC set status = 0 where trade_id = 100001;
边界:
1、如果cancel失败了如何处理呢?
思路:无限重试,确保成功