分布式事务

424 阅读6分钟

背景:在微服务、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	    status

1、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失败了如何处理呢?
	思路:无限重试,确保成功