vlambda博客
学习文章列表

分布式事务的前世今生(全篇)

分布式事务产生的原因

要搞清分布式事务,我们先想一个问题,分布式事务产生的原因是什么?

如图所示:

那么,我们如何实现分布式事务呢?根据分布式系统的CAP理论,要么是CP模式,要么是AP模式。三个无法同时实现。

  • CP是在保证数据强一致性前提下,尽量实现高可用(如过半写入)。

  • AP是在保证高可用的前提下,尽量实现数据一致性(如异步一致)。

分布式事务的前世今生(全篇)

在ACID里,I是最难实现的。Isolation包含四个层级,如下表所示。在XA中,一般可以做到RC就可以了。

事务隔离级别 事务隔离描述 解决的问题 不能解决的问题
最低1 Read Uncommitted 脏读
2 Read Committed 脏读 不可重复读
3 Repeated  Read 脏读、不可重复读 幻读
最高4 Serialization 脏读、不可重复读、幻读

对于MySQL而言,它是通过RC+MVCC实现RR的效果。但ACID整体上是不靠谱的,为啥?


因为ACID只是满足数据隔离性,但没有做法并发控制。


分布式事务真实使用场景

如果面向于实际使用场景,我们把上图分布式实现区分一下,结果如下:

分布式事务的前世今生(全篇)

也就是说,真实分布式场景中,80%是柔性事物,20%是刚性事务,即使在金融行业,也是这样。刚性事务通过2PC实现;柔性事物同步模式通过Sagas实现,异步模式通过事务消息实现。


分布式事务-刚性事务-2PC的实现原理

我们先看一下刚性事务的最终实现:2PC。如下图所示。在下图中,RM管理共享资源,如DB。TM负责管理全局事务,如分配事务唯一标识、监控事务的执行进度、并负责事务的提交、回滚、失败、恢复等。


分布式事务的前世今生(全篇)
2PC的缺点在于:它是同步阻塞模型、数据库锁定时间过长、全局锁(隔离级别串行化)并发低、不适合长事务场景(RM特别多的情况)。


3PC没有安全解决2PC的问题,但又引入了新的问题。由于实际场景几乎没人使用,因此我不做介绍。

分布式事务-柔性事务

在介绍了刚性事务2PC后,接下来我们介绍柔性事务。柔性事物的理念是BASE。

BASE理论指的是:

  • Basically Available(基本可用)

  • Soft state(柔性状态--->中间状态:业务中间态、数据中间态)

  • Eventually consistent(最终一致性)


我们举一个数据中间态的例子。一个转账业务,我们给转账业务数据分成两部分:可用金额要和冻结金额(并不是说,转账业务一定要按照下面模式设计,只是当对一致性要求比较高的时候,用下面的模式)。

分布式事务的前世今生(全篇)


分布式事务-柔性事务-BASE-Saga

接下来,我们看柔性事物的实现,先看Saga。

Saga本质是将一个分布式事务分为多个本地事务。每个本地事务只有执行和补偿。我们拿银行业余来说,有转账业务,转账业务的补偿事务就是:转账冲正,如下图所示。如果转账失败,就调用转账冲正进行事务补偿。

分布式事务的前世今生(全篇)

Saga的恢复模式分为:向后恢复(逆向补偿)和向前恢复(重试失败的事务)。


Sagas

业务逻辑层:基于AOP实现Proxy。@around

逻辑事务层增加注解,开启全局事务。

数据访问层:基于原则接口方法,在方法名加注释补偿方法名。@compensable(cancelMethod...)



Saga的开源实现:

ServiceComb(需要使用整套微服务,才能使用其中的Saga)

Seata,阿里开源的。SeataAT是Saga的优雅实现(使用状态机实现。上面的案例使用AOP实现)


在隔离性方面,Sagas隔离是通过业务中间态实现的。例如金融系统的中间账户。


例如郭德纲向大魏转账,真实模式是:

分布式事务的前世今生(全篇)

郭德纲向中间账户转账失败,那么中间账户会删除记录。如果一段时间没删除,例如5s,那么中间账户就会想大魏转账。


前面提到的转账和冲正是互为逆方法。同样,CRUD中,insert和delete也是互为逆方法。因此我们需要为update操作提供逆方法。


那么,如何为update提供优雅的事务补偿呢?使用Seata AT。Seata AT就是在Sagas的基础上实现了自动补偿。目前自动补偿的方案都是往这个方向努力,看谁支持的 sql 语法更多,支持的db类型更多。

Seata AT实现的方法如下图:

分布式事务的前世今生(全篇)


刚性事务与柔性事务的对比

经过上面内容的介绍,我们将分布式事务分类图进行进一步细化。

分布式事务的分类如下:

分布式事务的前世今生(全篇)


我们首先对刚性事务和柔性事务进行对比如下。

分布式事务的前世今生(全篇)

在柔性事物中,在柔性事物中,当业务失败还没来得及补偿时,是容易出现脏读的。隔离性支持到RU就可以。


分布式事务-BASE-事务消息的实现-半消息

大家不要认为金融行业的分布式事务一定是强一致的,实际情况中,强一致的比率不是特别高。例如蚂蚁金服2PC也没有大于20%。例如转账,用Saga、2PC都成。


那么,我们我们看一下事务消息的实现原理。

事务消息解决的是什么问题?我们先看一个常见的场景。

一个分布式事务由两个本地事务组成。两个事务之间有个MQ。A事务执行成功后(1),向MQ发送消息(2)。然后B事务消费消息(3)并执行本地事务(4)。

分布式事务的前世今生(全篇)

那么,事务消息解决的是什么问题呢?它解决的步骤1和步骤2的原子性问题。也即是说,把事务A写数据库和往MQ发消息这两件事,捏成一个原子事务。两件事要么一起成功、要么一起失败(下图篮圈)。

分布式事务的前世今生(全篇)


而分布式事务保证的是什么?是步骤1、2、3、4这四件事的原子性。也就是说,这四件事要么一起成功、要么一起失败(下图黄圈)。

分布式事务的前世今生(全篇)

所以说:事务消息不是分布式事务。但它大大简化了事务分布式模型。它将两次RPC调用(一次分布式事务至少两次RPC调用)简化成RPC+发消息。因此事务消息对业务非常友好。


具体实现如下图所示:


分布式事务的前世今生(全篇)

接下来,我们就上图步骤进行分析:

  1. MQ Producer(是业务逻辑)发送半消息给MQ Server。半消息的特点是:不会被消费,先存在MQ里。

  2. MQ Server告诉MQ Producer,半消息发送成功(这个通知是同步交付)。

  3. 访问数据库、执行本地事务。然后MQ Server把消息投递给MQ Subscriber。

  4. MQ Producer告诉MQ Server,投递还是不投递消息。例如告诉MQ Server投递,那么MQ Server把半消息变成确认消息,进行消息投递。

  5. MQ Server没有收到步骤4的确认,就回查MQ Producer,看消息是否需要投递。

  6. MQ Producer查数据库,看此前本地事务是否提交、是否成功。

  7. MQ Producer根据在数据库的查询结果,告诉MQ Server提交还是Rollback。然后MQ Server决定是丢弃还是投递消息。如果步骤1中,MQ  Producer发送半消息后,MQ  Producer挂了或者因为一些原因本地事务执行失败,那么步骤5-6回查,发现事务未成功,就会把半消息从MQ Server中删除。


京东购物24小时未支付订单自动取消的截图:

分布式事务的前世今生(全篇)



分布式事务-BASE-事务消息的实现-半消息-RocketMQ的实现

关于RocketMQ通过半消息实现事务消息原理,我们可以看得更细致些。


我们知道,在普通的MQ中,消息生产者将消息写入到CommitLog,然后Dispatcher将消息的索引信息放到Topic中以便消费者消费。那么,RocketMQ如何实现半消息?也就是消息放到Topic中不被消费?


RocketMQ有一个半消息Topic:RMQ_SYS_TRANS_HALF_TOPIC,它用于临时存放消息信息。这个Topic对消息消费者不可见。也就是说,消息生产者向Commitlog写入的如果是事务消息,Dispacher看到消息有transaction的标签,就会将消息的索引发送到RMQ_SYS_TRANS_HALF_TOPIC。也就是主题和队列名被替换了。但被替换的队列和主题名信息,会和索引一起存在RMQ_SYS_TRANS_HALF_TOPIC中。



除了半消息外,还有个OP消息主题RMQ_SYS_TRANS_OP_HALF_TOPIC。这个主题记录二阶段操作。OP消息包含如下两种:Rollback(只做记录)和Commit(根据备份信息重新构造消息并投递)。如下图所示其位置:

分布式事务的前世今生(全篇)

回查:对比HALF消息和OP消息主题。他们之间的差,就是需要回查的消息。如下图所示逻辑。

分布式事务的前世今生(全篇)

我们根据上图将半消息进行分析:

  1. 生产者发送事务消息,写到CommitLog。Dispatcher将其索引放入到RMQ_SYS_TRANS_HALF_TOPIC

  2. 消息生产者发送OP消息,这个消息先写入到CommitLog,然后被Dispatch到RMQ_SYS_TRANS_OP_HALF_TOPIC。

  3. 如果OP消息是Commit,MQ会将对这个半消息在CommitLog进行重构,然后再有Dispatcher重新投递到消费队列中。

  4. MQ对比RMQ_SYS_TRANS_OP_HALF_TOPIC和RMQ_SYS_TRANS_HALF_TOPIC两个队列,在后者超时的消息,到前者进行回查。到消息生产者回查事务状态(DB)。

  5. 然后消息生产者再度发送OP消息(针对回查结果Commit还是Rollback)

  6. 重新投递到消费队列中的消息被消费者消费。



事务消息需要业务方提供回查接口,对业务侵入较大。

在上面的方案中,所有事务一致性的保证,都由RockerMQ完成,就必然会有回查,业务就需要提供回查接口。


使用RocketMQ优点是方案通用,缺点是:需要业务代码实现消息回查,增加开发的工作量;发送消息幂等;消费端需要处理幂等。


因此,方案2是使用本地消息事务表。


分布式事务-BASE-事务消息的实现-事务消息表

我们可以通过客户端来保证事务的一致性。也就是说,在本地DB中放一个消息表,通过本地事务管理器维护事务消息表(扫描发送、清理)。

分布式事务的前世今生(全篇)


分布式事务的前世今生(全篇)

但是,上图只是实现了分布式事务的异步方式(有MQ),没有同步方式。接下来,我们看同时实现分布式事务同步和异步的模型。


柔性分布式事务的终极实现:Sagas+事务消息表

关于这种方式的实现,我们举例子说明。这是分布式事务的终极模型,也就是说,既能实现同步分布式事务(Saga),也能实现异步分布式事务(事务消息表)。

先看一张图:



分布式事务的前世今生(全篇)

我们结合京东购物场景看这张图:

  1. 在京东买大魏的书,放在购物车里,然后到支付界面。有哪位书太贵,犹豫了,没再操作。这时候,业务系统做的事情是:订单数据服务在两个DB表里写入信息,一个是orders表,同一个是mqMsgs,也就是消息表。表中存放的,就是支付消息。这两个表的写操作,在同一个数据库链接里完成。因此这两个表的写操作,就是一个本地事务。

  2. 订单业务逻辑服务从DB消息表中读取消息,写入到MQ中,这是通过起定时任务实现的(它是个子线程),业务逻辑层有MQClient。这个定时任务就是从DB表读信息,投递到MQ上。

  3. MQ Server给订单逻辑服务返回ACK。

  4. MQ给订单逻辑服务发ACK

  5. 订单业务逻辑调用订单数据访问服务,发起删除消息记录。

  6. 订单数据服务操作DB中的消息表,删除表中的数据。===>消息投递成功,就删除DB消息表中的记录。

  7. 订单业务逻辑从MQ中读取订单到期未支付消息(京东是24小时)。

  8. 订单业务逻辑服务调用订单数据服务,删除订单记录。

  9. 订单数据服务操作数据库表,删除数据库中orders表中的订单记录。


总结来说,上面的路子是:将向MQ发送消息的操作,通过AOP篡改为写DB,然后再通过业务逻辑层的定时任务定期从DB消息表中读消息并且放到MQ。




柔性分布式事务的终极实现:Sagas+事务消息表:架构分析

我们再举一个京东网购退货的例子:

分布式事务的前世今生(全篇)


分布式事务的前世今生(全篇)




在上图中,TDB会有两个表。也就是说,事务补偿能够成功实现,主要靠TDB中的这两张表!:

  • 事务状态表(记录事务组状态;txid、state、timestap)、

  • 事务调用组表(记录事务组中每一次调用和相关参数;txid、actionid、callmethod、pramatype、params)。


接下来,我们介绍上面两个表每个字段的含义:txid是主键、state表示事务状态(例如:1开始 2成功  3失败  4补偿成功 )、actionid是操作次数的id、callmethod是补偿接口、调用服务的类型(如web/RPC)、params是具体数据包的调度参数。


而事务补偿,就是当事务调用失败,事务拦截器修改事务组状态(state)。然后由TM发起,分布式事务补偿服务异步执行补偿。


上面的内容比较抽象,我们结合场景进行说明,还是以下图为例。

分布式事务的前世今生(全篇)


大魏在京东购物,这是一个分布式事务。这个时候,proxy生成事务组表的一行数据,并且记录(t1代表分布式事务1):

分布式事务的前世今生(全篇)

分布式事务的前世今生(全篇)

接下来,要正式干活儿了。Proxy记录事务A的调用信息,写入事务组表:

分布式事务的前世今生(全篇)

记录完毕后,A做本地事务:

分布式事务的前世今生(全篇)

A执行成功后,Proxy用类似的方式管理B,以此类推C。


Proxy在事务调用表中记录B的调用内容:

分布式事务的前世今生(全篇)


B本地事务执行:

分布式事务的前世今生(全篇)

Proxy在事务调用表中记录C的调用内容:

分布式事务的前世今生(全篇)

然后C执行。

分布式事务的前世今生(全篇)

当C本地事务执行成功后,Proxy将会修改TDB中的信息:

修改前:

分布式事务的前世今生(全篇)

修改后:

分布式事务的前世今生(全篇)

也就是说,标记分布式事务执行成功。



但是,如果在上面的步骤中,C执行失败呢?

首先,Proxy会将TDB中的事务组表进行如下修改,将状态从1开始修改为3失败:

修改前:

分布式事务的前世今生(全篇)

修改后:

分布式事务的前世今生(全篇)


接下来,Schedule(即TM)会扫描到(定期扫描)t1失败(状态为3)。然后Schedule发现T1在事务调用表有2个关联的本地事务(C做失败已经自动rollback)


分布式事务的前世今生(全篇)

接下来,根据事务调用表中的pramatype字段,RPC Client调用补偿接口,对事务进行补偿。

先补偿B:

分布式事务的前世今生(全篇)

再补偿A:

分布式事务的前世今生(全篇)

最后,补偿完毕后,proxy修改TBD中的事务组表,将state从3改成4,代表补偿成功。

分布式事务的前世今生(全篇)


柔性分布式事务的终极实现:Sagas+事务消息表:案例分析

我们上述逻辑,结合业务逻辑组件进行结合。这次我们把业务逻辑模块换一下。京东购物,选中货物后,库存会被锁住(A)、然后支付的时候,会选择红包或者京东卡,会有减红包或减京东卡余额的操作(C)、最后成功创建订单(C)。

具体参考下图: