分布式事务
原始定义:在分布式服务环境下, 多个服务同时操作多个数据源的事务处理机制。
追求结果:让多个服务在协同执行业务的时候,数据具有一致性或者最终一致性。
CAP
在一个分布式的系统中,当涉及到共享数据问题时,以下三个特性最多只能满足其中两个:
- 一致性(Consistency):
- 代表在任何时刻、任何分布式节点中,我们所看到的数据都是没有矛盾的。
- 保证数据一定是一致的,对的;
- 可用性(Availability):
- 代表系统不间断地提供服务的能力。
- 保证系统能用
- 分区容错性(Partition Tolerance):
- 代表分布式环境中,当部分节点因网络原因而彼此失联(即与其他节点形成“网络分区”)时,系统仍能正确地提供服务的能力。
- 就算网络出了问题(分区),系统仍能正确地提供服务的能力。
- 脑裂的问题。
共享数据问题的CAP
- 保证一致性和可用性,则会放弃分区容错性(CA without P)
- 如果保证一致性和可用性,当节点直接的通讯出现问题的时候,为了保证一致性,则需要同步完成,则分区容错不存在。自然就需要放弃分区容错
- 保证一致性和分区容错,则会放弃可用性(CP without A)
- 如果是保证一致性和分区容错,当一旦网络出现问题,出现分区,为了保证一致性,节点之间的信息同步时间可以无限制地延长,这样,自然就需要放弃可用性了。
- 保证可用性和分区容错,则会放弃一致性(AP without C)
- 如果保证可用性和分区容错,当一旦网络出现问题,出现分区,为了保证可用性,则不会等数据同步完成,就提供服务,则一致性就无法保证。
分布式服务下的CAP举例
一个电商系统中存在 订单 和 库存 俩个系统。在创单过程中,如果订单创建成功,库存也必定扣减成功。
- 如果你要保证CA,在机房网络出现问题的时候,订单不能访问到库存,为了数据一致,你的订单系统也必定是不能用的。而库存系统无法确定某个库存是否需要扣减的内容, 自然也无法使用,即失去了分区容错。
- 如果你要保证CP,那么在你的库存系统出现问题的时候,就需要等待库存系统回复正常,完成库存扣减,这样你的订单系统自然无法提供服务,即失去了可用性。
- 如果你要保证AP,这样,在库存系统出现问题的时候,订单系统依然可以正常运行,但是,这样的话,库存数据和订单系统的数据就不一致了,等于放弃了数据的一致性。
一致性
- CAP、CP、CA, 本地事务ACID强一致性
- AP弱一致性性
- 可靠消息队列 最终一致性
分布式事务角色
- TC(Transaction Coordinator) - 事务协调者
- 维护全局和分支事务的状态,驱动全局事务提交或回滚。
- 在seata 中为seata-server ,一般为第三方服务
- TM(Transaction Manager) - 事务管理器
- 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- 一般为服务调用组织方
- RM(Resource Manager) - 资源管理器
- 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
- 一般为服务调用方,在XA模式中,一般为数据库
XA
基于数据库实现
两阶段提交
1. 准备阶段
- 协调者询问事务的所有参与者是否准备好提交,如果已经准备好提交回复 Prepared,否则回复 Non-Prepared。
- 对于数据库来说,准备操作是在重做日志中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record。这意味着在做完数据持久化后并不会立即释放隔离性,也就是仍继续持有锁,维持数据对其他非事务内观察者的隔离状态。
- 简单说,执行操作,不提交事务 2. 提交阶段
- 协调者如果在准备阶段收到所有事务参与者回复的 Prepared 消息,就会首先在本地持久化事务状态为 Commit,然后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者都会将自己的事务状态持久化为“Abort”之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。
2PC的前提
- 网络需要完全可靠
- 必须假设因为网络分区、机器崩溃或者其他原因而导致失联的节点最终能够恢复,不会永久性地处于失联状态。
2PC的缺点
- 单点问题 依靠协调者,协调者宕机,则会一直等待,导致数据库问题
- 性能问题 ,依靠数据库,则会可能出现长事务,一旦有一个慢事务,整个系统会被拖垮
- 一致性的问题, 当网络稳定性和宕机恢复能力的假设不成立时,两段式提交可能会出现一致性问题。
改进3PC
将准备阶段分为了两个阶段
- CanCommit
- 询问阶段,协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成
- PreCommit
- 执行操作
- 3PC降低来参与者的阻塞范围,但是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的通信,在这种情况下,该参与者依然会进行事务的提交,就会出现数据的不一致。
AT
简介
- seata 实现的一种模式,改进了XA模式事务占用资源时间过长的问题。
- 在业务数据提交时,自动拦截所有 SQL,分别保存 SQL 对数据修改前后结果的快照,通过本地事务一起提交到操作的数据源中,这就相当于自动记录了重做和回滚日志。
- 如果分布式事务成功提交了,那么我们后续只需清理每个数据源中对应的日志数据即可;而如果分布式事务需要回滚,就要根据日志数据自动产生用于补偿的“逆向 SQL”。
- 通过存储操作前数据的 UNDO_LOG 表来完成事务的回滚。
整体机制
- 一阶段:
- 业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交完成,在清理回滚日志记录。
- 回滚通过一阶段的回滚日志进行反向补偿。
问题
- 如果有 后门程序直接修改数据库,会导致AT模式的不可用。
- AT模式如果要实现写隔离 ,可以使用全局锁,要求本地事务提交之前,一定要先拿到针对修改记录的全局锁后才允许提交,而在没有获得全局锁之前就必须一直等待。这种设计以牺牲一定性能为代价,避免了在两个分布式事务中,数据被同一个本地事务改写的情况,从而避免了脏写。
- 一旦一个方法使用了AT模式,则其他所有操作的都必须实现AT模式。
- AT模式的默认隔离级别是读未提交,因为在分支事务是直接提交的,所以会造成脏读现象。
优点
- 代码入侵量小,实现速度快
TCC
介绍
- TCC是服务化的两阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现
- Try操作作为一阶段,负责资源的检查和预留
- Confirm操作作为二阶段提交操作,执行真正的业务,Cancel是预留资源的取消
- 对于我们来说,资源管理器一般是每个服务都有自己的资源管理器,进行资源管理,即TCC服务
业务操作两阶段
- 在接入TCC前,业务操作只需要一步就能完成
- 接入TCC之后,需要考虑如何将其分成2阶段完成,把资源的检查和预留放在一阶段的Try操作中进行,把真正的业务操作的执行放在二阶段的Confirm操作中进行
- 例如库存扣减,将Try阶段需要将操作数据存储在本地表,然后在Confirm阶段真正提交到数据库
- 例如订单创建,在Try阶段 可以先把我们的订单创建之后状态设置中间状态,在Confirm阶段我们将订单设置为正常的状态
- 例如订单修改,在Try阶段,可以先把我们需要修改数据存储在本地表,在Confirm阶段,将真正修改的数据写入数据库
空回滚
- TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚。TCC服务在实现时应当允许空回滚的执行
- 事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作
- 解决方案 可以根据 保存 全局事务ID XID 的的状态,然后根据XID的状态来确定是否执行回滚操作,如果没有执行过try,则不用执行回滚操作
事务悬挂
- 用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求
- 即在上面的情况下,已经执行了空回滚之后,Try请求才到来,这个时候如果执行了Try请求,那么之后肯定没有对应的提交和回滚操作,这样这个事务就被悬挂起来了,永久的,消耗资源
- 解决方式,依然是事务状态表
幂等控制
- 无论是网络数据包重传,还是异常事务的补偿执行,都会导致TCC服务的Try、Confirm或者Cancel操作被重复执行
- 用户在实现TCC服务时,需要考虑幂等控制,即Try、Confirm、Cancel 执行次和执行多次的业务结果是一样的
业务数据可见性
- TCC服务的一阶段Try操作会做资源的预留,在二阶段操作执行之前,如果其他事务需要读取被预留的资源数据,那么处于中间状态的业务数据该如何向用户展示,需要业务在实现时考虑清楚
- 通常的设计原则是“宁可不展示、少展示,也不多展示、错展示”
并发控制
- TCC服务的一阶段Try操作预留资源之后,在二阶段操作执行之前,预留的资源都不会被释放;如果此时其他分布式事务修改这些业务资源,会出现分布式事务的并发问题
- 用户在实现TCC服务时,需要考虑业务数据的并发控制,尽量将逻辑锁粒度降到最低,以最大限度的提高分布式事务的并发性
SAGA
介绍
- SAGA 事务基于数据补偿代替回滚的解决
- SAGA 的意思是“长篇故事、长篇记叙、一长串事件”
SAGA目的
是为了避免大事务长时间锁定数据库的资源,后来才逐渐发展成将一个分布式环境中的大事务,分解为一系列本地事务的设计模式。
SAGA执行过程
Saga对比TCC少了一步try的操作,TCC无论最终事务成功失败都需要与事务参与方交互两次。而Saga在事务成功的情况下只需要与事务参与方交互一次, 如果事务失败,需要交互两次
提交
- 一部分是把大事务拆分成若干个小事务,将整个分布式事务 T 分解为 n 个子事务,我们命名为 T1、T2、…、Ti、…、Tn。每个子事务都应该、或者能被看作是原子行为。如果分布式事务 T 能够正常提交,那么它对数据的影响(最终一致性)就应该与连续按顺序成功提交子事务 Ti 等价。
- 另一部分是为每一个子事务设计对应的补偿动作,我们命名为 C1、C2、…、Ci、…、Cn。Ti 与 Ci 必须满足以下条件:
- Ti 与 Ci 都具备幂等性
- Ti 与 Ci 满足交换律(Commutative),即不管是先执行 Ti 还是先执行 Ci,效果都是一样的
- Ci 必须能成功提交,即不考虑 Ci 本身提交失败被回滚的情况,如果出现就必须持续重试直至成功,或者要人工介入
- 如果 T1 到 Tn 均成功提交,那么事务就可以顺利完成
恢复模式
- 正向恢复(Forward Recovery)
- 如果 Ti 事务提交失败,则一直对 Ti 进行重试,直至成功为止(最大努力交付)。这种恢复方式不需要补偿,适用于事务最终都要成功的场景,比如在别人的银行账号中扣了款,就一定要给别人发货。正向恢复的执行模式为:T1、T2、…、Ti(失败)、Ti(重试)…、Ti+1、…、Tn。
- 反向恢复(Backward Recovery)
- 如果 Ti 事务提交失败,则一直执行 Ci 对 Ti 进行补偿,直至成功为止(最大努力交付)。这里要求 Ci 必须(在持续重试后)执行成功。反向恢复的执行模式为:T1、T2、…、Ti(失败),Ci(补偿)、…、C2,C1。
事务消息
本质
- 本地事务执行完成之后,消息一定会发送出去,一定会通知到消息订阅方
事务消息发送步骤如下
- 发送方将半事务消息发送至消息队列 RocketMQ 版服务端
- 消息队列 RocketMQ 版服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息
- 发送方开始执行本地事务逻辑
- 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback)
- 服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息
- 服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息
事务消息回查步骤如下:
- 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查
- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
- 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行操作
总结
分布式事务的关键因素
- 多阶段提交
- 重试机制
详情总结
| 优点 | 缺点 | 过程 | |
|---|---|---|---|
| XA/2PC | 强一致性 | 强依赖数据库,,资源锁定时间过长,很有可能出现死锁 | 第一次执行执行数据更改,不提交事务;第二次提交/回滚事务 |
| XA/3PC | 强一致性 | 多一次预请求,在某些情况下,有可能性能会提升,有可能性能下降 | 第一次询问是否可以执行事务;第二次执行执行数据更改,不提交事务;第三次提交/回滚事务 |
| AT | 实现简单,没有业务入侵 | 数据隔离性不强,有可能脏读,脏写(在没有全局锁的时候) | 第一次将业务代码和undo_log表的数据同时提交事务;第二次完成事务/使用undo_log表的数据进行回滚 |
| TCC | 几乎不会涉及到锁和资源的争用,具有很高的性能潜力 | 编码工作量大,复杂业务实现困难 | 第一次执行预请求方法;第二次执行回滚方法/提交事务方法 |
| Saga | 一阶段提交本地数据库事务,无锁,高性能;补偿服务易于理解,易于实现 | Sage无法保证隔离性,需要额外加锁保证 | 把大事务拆分成若干个子事务,T1、T2、…、Ti、…、Tn;每一个子事务都对应一个补偿动作,C1、C2、…、Ci、…、Cn |
| 事务消息 | 实现简单,没有业务侵入,耦合性低 | 无法实现强一致性,只能是最终一致性,而且没有办法做资源的锁定 | 第一阶段发送半消息——执行本地事务方法;第二阶段提交或取消半消息 |
转自 yuque.com