分布式事务
简介
分布式事务是解决分布式场景下数据一致性的问题。
原理
二阶段提交
两阶段提交(2PC)是分布式事务的一种实现方案。
两阶段提交协议把分布式事务分成两个过程,分别是准备阶段和提交阶段,准备阶段和提交阶段都是由事务管理器(协调者)发起,资管管理器(事务参与者)接收协调者的指令完成事务的执行动作。
两阶段具体分工如下:
准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写redo或者undo日志(这也是前面提起的Write-Ahead Log的一种),然后锁定资源,执行操作,但是并不提交。
提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源。
三阶段提交
三阶段提交是分布式事务的一种实现方案,是两阶段提交的优化。
两阶段存在阻塞问题和一致性问题。
- 阻塞
- 两阶段提交中协调者发起的准备指令和提交指令必须收到所有参与者的返回才能决定后续的状态,否则将持续处于阻塞状态,此时占用的资源会被一直锁定,长期占据的资源会对性能有较大的影响。
- 协调者在执行事务的过程中如果发生故障,则所有的参与者都会陷入阻塞等待后续指令。
- 不一致
- 协调者发送提
- 交指令,部分参与者成功提交了事务,部分参与者未能接受提交指令,此时多个参与者的状态就出现了不一致的情况。
- 如果部分参与者在接收到提交指令之后发生宕机,而此时协调者也发生了宕机,后续选举出来的协调者的继任者无法确定所有参与者的状态,此时会出现故障。
三阶段针对两阶段存在的两个问题,阻塞和不一致问题给出了解决方案,其中,阻塞问题通过引入超时机制解决,不一致问题通过引入新的阶段预提交环节解决。
三阶段提交将分布式事务分成三个环节,准备阶段,预提交阶段,提交阶段。
本地消息表
本次消息表是一种最终一致性方案。
将参与分布式事务的多个系统称为参与者,主参与者和其他业务参与方需要在逻辑以及数据上保存一致,本地消息表是在主参与方的数据库中建一个消息表,依靠消息表的重试实现最终一致性的方案。
本次消息表也有两个阶段:
第一个阶段是本次消息的存储,本次消息是一个独立的消息表,该表和业务表在同一个数据库中,所以可以依靠本次事务的ACID保证业务数据和消息数据同时存储成功或者失败。
第二个阶段依靠本地消息进行重试,直至所有的业务参与方都执行成功。
事务消息
事务消息也可以称之为两阶段消息,其操作流程如下:
在业务操作之前首先发送半消息到消息队列。
业务操作成功取消半消息。
如果业务操作失败发送逆向消息进行数据回退。
如果二阶段消息未收到,会通过半消息的延迟消费进行状态回查。
TCC
TCC是Try、Confirm、Cancle三个阶段,可以将TCC看作是两阶段提交或者三阶段提交在业务层的实现。
TCC将业务操作拆分成Try、Confirm、Cancel,首先先执行Try,如果执行成功接着执行Confirm操作,如果执行过程中出了问题,则执行逆向的Cancel操作。
TCC中的Try是指在业务层锁定资源,所以对账户型的资源参与方(库存,资金账户等)较为合适,TCC将资源分为可用资源,锁定资源,已使用资源,Try阶段将可少可用资源,增加锁定资源,而在Confirm阶段则将这部分锁定资源转换为已用资源。
通过对资源的提前占用可以减少并发场景下资源超发的情况,而且锁定资源和解锁资源不会影响真正的资源转移,所以无论是Confirm操作还是Cancel操作的成本都相对较低。
TCC本质是简化版的三阶段提交协议,解决了两阶段提交协议的阻塞问题,但是没有解决极端情况下会出现不一致和脑裂的问题。只能通过自动化补偿手段,将需要人工处理的不一致情况降到到最低。
Sega
Saga的概念来源于一篇数据库论文Sagas ,一个 Saga 事务是由多个短事务构成的的长时的事务,这些短事务分别有正向的执行方法以及逆向的回滚方法。 在分布式事务场景下,一个Saga分布式事务看做是一个由多个本地事务组成的事务,每个本地事务都有一个与之对应的补偿事务,类似于commit和rollback。在Saga事务的执行过程中,如果某一步执行出现异常,Saga事务会被终止,同时会调用本地事务对应的补偿事务完成相关的恢复操作,这样保证Saga相关的本地事务要么都是执行成功,要么通过补偿恢复成为事务执行之前的状态。
Sega的实现可以分为状态机实现和非状态机的实现。
状态机实现会通过状态机描述每个状态下对应的下一个动作:
- 在某一个子事务结束后,根据这个子事务的执行结果,决定下一步的操作。
- 子事务执行的结果可以保存到状态机,作为后续子事务的输入。
- 没有依赖的子事务之间可以并发执行。
状态机实现Sega优点是功能强大,可以自由灵活的编排事务过程。
缺点是接口入侵强,只能使用特定的输入输出接口参数类型,在云原生时代,对强类型的gRPC不友好。
非状态机的实现一般采用函数接口的方式,定义全局事务下的各个分支事务,其更加简单,但是灵活性较状态机实现略差。
非状态机实现的 Sega 框架有eventuate,dtm等。
Seata
Seata是阿里巴巴开源的分布式解决方案,Seata采用CS架构模式。 Seata 定义了 3 个核心组件:
- TM(Transaction Manager):事务管理器,事务的发起者,负责定义全局事务的范围,并根据 TC 维护的事务状态(全局事务和分支事务),做出推进事务的指令,例如开始事务、提交事务、回滚事务等。
- TC(Transaction Coordinator):事务协调器,事务的协调者,由 Seata 服务器承担,主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
- RM(Resource Manager):资源管理器,资源的管理者,资源是指参与分布式事务的各个本地资源,一般指数据库。资源管理器负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
全局事务的唯一标识称为 XID ,XID 可以在服务的调用链路中传递,绑定到服务的事务上下文中。
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式。
AT 模式
AT 模式是一种自动化无侵入的解决方案,使用者不需要感知分布式事务的存在。
AT 模式的整体机制也是两阶段:
一阶段,本地事务就进行本次资源的提交和回滚操作,同步记录回滚日志,一阶段结束之后就会释放本地链接以及本地事务的锁资源。
undo_log和本地资源位于同一个数据库,其表结构如下:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;
二阶段,提交全局事务,如果执行回滚操作则通过一阶段的回滚日志执行恢复数据。
因为AT在第一阶段已经释放了本地锁,所以AT只能实现读已提交的隔离级别,所以会出现脏写等问题,为了解决脏写问题,需要提升隔离性,
- 脏读
- select语句加for update,代理方法增加@GlobalLock+@Transactional或@GlobalTransactional
- 脏写
- 必须使用@GlobalTransactional 注:如果你查询的业务的接口没有@GlobalTransactional 包裹,也就是这个方法上压根没有分布式事务的需求,这时你可以在方法上标注@GlobalLock+@Transactional 注解,并且在查询语句上加 for update。 如果你查询的接口在事务链路上外层有@GlobalTransactional注解,那么你查询的语句只要加for update就行。设计这个注解的原因是在没有这个注解之前,需要查询分布式事务读已提交的数据,但业务本身不需要分布式事务。 若使用@GlobalTransactional注解就会增加一些没用的额外的rpc开销比如begin 返回xid,提交事务等。GlobalLock简化了rpc过程,使其做到更高的性能。