分布式事务

204 阅读15分钟

分布式事务

原始定义:在分布式服务环境下, 多个服务同时操作多个数据源的事务处理机制。

追求结果:让多个服务在协同执行业务的时候,数据具有一致性或者最终一致性。

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模式中,一般为数据库

image.png

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服务

image (1).png

业务操作两阶段

  • 在接入TCC前,业务操作只需要一步就能完成
  • 接入TCC之后,需要考虑如何将其分成2阶段完成,把资源的检查和预留放在一阶段的Try操作中进行,把真正的业务操作的执行放在二阶段的Confirm操作中进行
    • 例如库存扣减,将Try阶段需要将操作数据存储在本地表,然后在Confirm阶段真正提交到数据库
    • 例如订单创建,在Try阶段 可以先把我们的订单创建之后状态设置中间状态,在Confirm阶段我们将订单设置为正常的状态
    • 例如订单修改,在Try阶段,可以先把我们需要修改数据存储在本地表,在Confirm阶段,将真正修改的数据写入数据库

image (2).png

空回滚

  • TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚。TCC服务在实现时应当允许空回滚的执行
  • 事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作
  • 解决方案 可以根据 保存 全局事务ID XID 的的状态,然后根据XID的状态来确定是否执行回滚操作,如果没有执行过try,则不用执行回滚操作

image (3).png

事务悬挂

  • 用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求
  • 即在上面的情况下,已经执行了空回滚之后,Try请求才到来,这个时候如果执行了Try请求,那么之后肯定没有对应的提交和回滚操作,这样这个事务就被悬挂起来了,永久的,消耗资源
  • 解决方式,依然是事务状态表

image (4).png

幂等控制

  • 无论是网络数据包重传,还是异常事务的补偿执行,都会导致TCC服务的Try、Confirm或者Cancel操作被重复执行
  • 用户在实现TCC服务时,需要考虑幂等控制,即Try、Confirm、Cancel 执行次和执行多次的业务结果是一样的

image (5).png

业务数据可见性

  • 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。

事务消息

13349286-16d72117c80a4a62.png

本质

  • 本地事务执行完成之后,消息一定会发送出去,一定会通知到消息订阅方

事务消息发送步骤如下

  1. 发送方将半事务消息发送至消息队列 RocketMQ 版服务端
  2. 消息队列 RocketMQ 版服务端将消息持久化成功之后,向发送方返回 Ack 确认消息已经发送成功,此时消息为半事务消息
  3. 发送方开始执行本地事务逻辑
  4. 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback)
    • 服务端收到 Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息
    • 服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息

事务消息回查步骤如下:

  1. 在断网或者是应用重启的特殊情况下,上述步骤 4 提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查
  2. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
  3. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行操作

总结

分布式事务的关键因素

  1. 多阶段提交
  2. 重试机制

详情总结

优点缺点过程
XA/2PC强一致性强依赖数据库,,资源锁定时间过长,很有可能出现死锁第一次执行执行数据更改,不提交事务;第二次提交/回滚事务
XA/3PC强一致性多一次预请求,在某些情况下,有可能性能会提升,有可能性能下降第一次询问是否可以执行事务;第二次执行执行数据更改,不提交事务;第三次提交/回滚事务
AT实现简单,没有业务入侵数据隔离性不强,有可能脏读,脏写(在没有全局锁的时候)第一次将业务代码和undo_log表的数据同时提交事务;第二次完成事务/使用undo_log表的数据进行回滚
TCC几乎不会涉及到锁和资源的争用,具有很高的性能潜力编码工作量大,复杂业务实现困难第一次执行预请求方法;第二次执行回滚方法/提交事务方法
Saga一阶段提交本地数据库事务,无锁,高性能;补偿服务易于理解,易于实现Sage无法保证隔离性,需要额外加锁保证把大事务拆分成若干个子事务,T1、T2、…、Ti、…、Tn;每一个子事务都对应一个补偿动作,C1、C2、…、Ci、…、Cn
事务消息实现简单,没有业务侵入,耦合性低无法实现强一致性,只能是最终一致性,而且没有办法做资源的锁定第一阶段发送半消息——执行本地事务方法;第二阶段提交或取消半消息

转自 yuque.com