在分布式事务之CAP定理一文中,我们了解到一个分布式系统不可能同时满足数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)。
现实情况下,我们面对的是一个不可靠的网络、有一定概率宕机的设备,这两个因素都会导致Partition,因而分布式系统实现中 P 是一个必须项,而不是可选项。
对于分布式系统工程实践,CAP理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性。
因此,我们需要在C和A之间进行取舍:
1994 年,X/Open 组织(即现在的 Open Group )定义了分布式事务处理的DTP 模型。
该模型包括这样几个角色:
在该模型中,一个分布式事务(全局事务)可以被拆分成许多个本地事务,运行在不同的AP和RM上。每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功,若有一个本地事务失败,则所有其它事务都必须回滚。但问题是,本地事务处理过程中,并不知道其它事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态。
因此,各个本地事务的通信必须有统一的标准,否则不同数据库间就无法通信。XA就是 X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。
要想控制分布式事务,仅仅靠厂商实现XA规范是不够的,所以引入了两阶段提交协议。
指事务的提交分为两个阶段:准备阶段和执行阶段。
两阶段提交用来协调参与一个更新中的多个服务器的活动,以防止分布式系统部分失败时产生数据的不一致性。例如,如果一个更新操作要求位于三个不同结点上的记录被改变,且其中只要有一个结点失败,另外两个结点必须检测到这个失败并取消它们所做的改变。
二阶段提交协议将全局事务拆分为两个阶段来执行:
这个过程中需要一个协调者(coordinator),还有事务的参与者(voter)。
投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行成功的信息(agree)
提交阶段:协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。
投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行结果,但只要有一个参与者返回的是Disagree,则说明执行失败。
提交阶段:协调组发现有一个或多个参与者返回的是Disagree,认为执行失败。于是向各个事务参与者发出abort指令,各个事务参与者回滚事务。
单点故障问题
2PC的缺点在于不能处理fail-stop形式的节点failure. 比如下图这种情况。
假设coordinator和voter3都在Commit这个阶段crash了, 而voter1和voter2没有收到commit消息. 这时候voter1和voter2就陷入了一个困境。因为他们并不能判断现在是两个场景中的哪一种:
阻塞问题
在准备阶段、提交阶段,每个事务参与者都会锁定本地资源,并等待其它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了。
可能发生死锁。
面对二阶段提交的上述缺点,后来又演变出了三阶段提交,但是依然没有完全解决阻塞和资源锁定的问题,而且引入了一些新的问题,因此实际使用的场景较少。
对事务有强一致性要求,对事务执行效率不敏感,并且不希望有太多代码侵入。
TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。
它本质是一种补偿的思路。事务运行过程包括三个方法:
执行分两个阶段:
粗看似乎与两阶段提交没什么区别,但其实差别很大:
优势
TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其它事务的执行结果。而如果其它事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
缺点
使用场景
为了避免MQ事务消息方案中消息发送失败或丢失,我们可以把消息持久化到数据库中。实现时有简化版本和解耦合版本两种方式。
事务发起者:
事务接收者:
额外的定时任务
优点:
缺点:
为了解决上述问题,我们会引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为,大概的模型如下:
一次消息发送的时序图:
事务发起者A的基本执行步骤:
消息服务本身提供下面的接口:
事务参与者B的基本步骤:
优点:
缺点:
RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制:
经过上面的两种确认机制,可以确保从消息生产者到消费者的消息安全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最终一致性。
优点:
业务相对简单,不需要编写三个阶段业务;
是多个本地事务的结合,因此资源锁定周期短,性能好;
缺点:
代码侵入;
依赖于MQ的可靠性;
消息发起者可以回滚,但是消息参与者无法引起事务回滚;
事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况;
针对事务无法回滚的问题,有人提出说可以再事务参与者执行失败后,再次利用MQ通知消息服务,然后由消息服务通知其他参与者回滚。那么,恭喜你,你利用MQ和自定义的消息服务再次实现了2PC 模型,又造了一个大轮子;
2019年 1 月份,Seata 开源了 AT 模式。AT 模式是一种无侵入的分布式事务解决方案。可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。
在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。可以参考Seata的官方文档。
基本原理:
流程图:
有没有感觉跟TCC的执行很像,都是分两个阶段:
但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部有Seata自己实现了。也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务。
在一阶段,Seata 会拦截业务SQL,首先解析 SQL 语义,找到业务SQL要更新的业务数据,在业务数据被更新前,将其保存成before image
,然后执行业务SQL更新业务数据,在业务数据更新之后,再将其保存成after image
,最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
这里的before image
和after image
类似于数据库的undo和redo日志,但其实是用数据库模拟的。
二阶段如果是提交的话,因为业务SQL在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的业务 SQL,还原业务数据。回滚方式便是用before image
还原业务数据;但在还原前要首先要校验脏写,对比数据库当前业务数据和 after image
,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
不过因为有全局锁机制,所以可以降低出现脏写的概率。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写业务SQL,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
详细架构和流程
Seata中的几个基本概念:
TC(Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)。
TM(Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM(Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
架构图
一阶段:
before_image
;after_image
;undo_log
并写入数据库;二阶段:
before_image
和after_image
信息,释放全局锁;before_image
,清除before_image
和after_image
;