一文快速了解分布式事务

182 阅读18分钟

什么是分布式事务

事务

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。

事务的特性:ACID

  • A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

  • D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

本地事务

本地事务是最基础的一种事务解决方案,只适用于单个服务、单个数据源的场景。

从应用角度看,它是直接依赖于数据源本身提供的事务能力来工作的,在程序代码层面,最多只能对事务接口做一层标准化的包装(如 JDBC 接口),并不能深入参与到事务的运作过程当中,事务的开启、终止、提交、回滚、嵌套、设置隔离级别,乃至与应用代码贴近的事务传播方式,全部都要依赖底层数据源的支持才能工作。

分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务产生的场景

1.数据库拆分

业务数据规模的快速发展,数据量越来越大,单库单表会逐渐成为瓶颈。对数据库进行水平拆分,将原单库单表拆分成数据库分片。分库分表之后,原来在一个数据库上就能完成的写操作,可能就会跨多个数据库,这就产生了跨数据库事务问题。

2.服务层拆分

为解决单体应用的性能瓶颈,或实现业务上的解耦,将一个单体服务拆分成若干服务。带来的问题是一个写操作有可能会涉及到多个服务的多个数据库的操作,就需要解决这些操作的事务性问题。

分布式事务理论基础

XA协议

XA (Extended Architecture ) 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。

XA 规范 描述了全局的事务管理器与局部的资源管理器之间的接口。

XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。

  • AP 应用程序,应用,事务的发起者。

  • RM 资源管理器,比如数据库。

  • TM 事务管理器,协调者,和每个 RM 通信。

2PC-二阶段提交

  • 1.准备阶段

事务协调者给每个参与者发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

  • 2.提交阶段

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。

二阶段提交的缺点

  • 单点问题:try完后,如果协调者发生宕机,没有正常发送 commit 或者 rollback 的指令,所有参与者要一直等待资源释放。

  • 性能问题:两段提交过程中,所有参与者相当于被绑定成为一个统一调度的整体,期间要经过两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久化,提交阶段在日志写入 Commit Record),整个过程将持续到参与者集群中最慢的那一个处理操作结束为止,这决定了两段式提交的性能通常都较差。

  • 一致性风险:如果协调者在发出准备指令后,根据收到各个参与者发回的信息确定事务状态是可以提交的,协调者会先持久化事务状态,并提交自己的事务,如果这时候网络忽然被断开,无法再通过网络向所有参与者发出 Commit 指令的话,就会导致部分数据(协调者的)已提交,但部分数据(参与者的)既未提交,也没有办法回滚,产生了数据不一致的问题。

3PC-三阶段提交

为了解决两段式提交协议的部分缺陷,具体地说是协调者的单点问题和准备阶段的性能问题,后续又发展出了“三段式提交”(3 Phase Commit,3PC)协议。

三阶段提交相对二阶段的主要变更点

1.准备阶段再细分为两个阶段,分别称为 CanCommit、PreCommit。

新增的 CanCommit 是一个询问阶段,协调者让每个参与的数据库根据自身状态,评估该事务是否有可能顺利完成。将准备阶段一分为二的理由是这个阶段是重负载的操作,一旦协调者发出开始准备的消息,每个参与者都将马上开始写重做日志,它们所涉及的数据资源即被锁住,如果此时某一个参与者宣告无法完成提交,相当于大家都白做了一轮无用功。所以,增加一轮询问阶段,如果都得到了正面的响应,那事务能够成功提交的把握就比较大了,这也意味着因某个参与者提交时发生崩溃而导致大家全部回滚的风险相对变小。

2.引入超时机制,如果参与者没有能等到 DoCommit 的消息的话,默认提交事务,避免了协调者单点问题的风险。

三阶段提交的缺点

  • 性能问题:在事务需要回滚的场景中,三段式的性能通常是要比两段式好很多的,但在事务能够正常提交的场景中,两者的性能都依然很差,甚至三段式因为多了一次询问,还要稍微更差一些。

  • 一致性风险:三段式仍然存在一致性的风险,比如,进入 PreCommit 阶段之后,协调者发出的指令不是 Ack 而是 Abort,而此时因网络问题,有部分参与者直至超时都未能收到协调者的 Abort 指令的话,这些参与者将会错误地提交事务,这就产生了不同参与者之间数据不一致的问题。

CAP定理

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • 一致性(Consistency):代表数据在任何时刻、任何分布式节点中所读到的都是符合预期的数据。简单理解成“写操作之后所有分布式节点的读操作,必须返回该值”。

  • 可用性(Availability):代表系统不间断地提供服务的能力。简单理解成“只要收到用户的请求,服务器就必须给出合理的回应。”

  • 分区容忍性(Partition Tolerance):代表分布式环境中部分节点因网络原因而彼此失联后,即与其他节点形成“网络分区”时,系统仍能正确地提供服务的能力。简单理解成“部分节点故障不影响整体使用。”

一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。

选择放弃一致性的 AP 系统目前是设计分布式系统的主流选择,而对于一致性也不是完全放弃,而是去追求最终一致性。

BASE理论

Basically Available(基本可用)

基本可用就是假设系统某个模块出现了不可预知的故障,但其他模块依旧可用,例如商城双十一活动时,评论模块出现故障,但不会影响交易、商品等核心模块的流程使用。

Soft State(软状态)

软状态指的是允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

Eventually Consistent(最终一致性)

上面讲到的软状态不可能一直是软状态,必须有时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性,因此所有客户端对系统的数据访问最终都能够获取到最新的值,而这个时间期限取决于网络延时,系统负载,数据复制方案等因素。

在 CAP 中的一致性要求在任何时间查询每个节点数据都必须一致,它强调的是强一致性,强一致性的事务叫刚性事务;而最终一致性是允许在一段时间内每个节点的数据不一致,但是经过一段时间每个节点的数据必须一致,它强调的是最终数据的一致性,这种事务叫柔性事务

分布式事务常用解决方案

本地消息表

本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理,事务发起方需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮询事务消息表的数据发送事务消息;事务接收方基于消息中间件消费事务消息表中的事务,成功后修改调用方消息状态。

不使用MQ的方案:

调用方直接轮询事务消息表,直接调用对应业务的接口,成功则更新消息状态,不成功则继续重试(也可以支持手工重试)。

本地消息表的优缺点

(1)优点:

  • 方案轻量,容易实现

  • MQ不需要支持事务特性

(2)缺点:

  • 不具备隔离性,会存在“超售问题”

  • 消息数据与业务数据同库,占用业务系统资源,量大会影响数据库性能

事务消息

RocketMQ

RocketMQ消息交互流程

事务消息交互流程如下图所示。

事务消息发送步骤如下:

  1. 生产者将半事务消息发送至消息队列RocketMQ版服务端。

  2. 消息队列RocketMQ版服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息为半事务消息。

  3. 生产者开始执行本地事务逻辑。

  4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:

    1. 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
    2. 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
  5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。

事务消息回查步骤如下:

  1. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。

  2. 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

本地消息表的优缺点

(1)优点:

  • 方案轻量,容易实现

  • 可用性和性能高

(2)缺点:

  • 不具备隔离性,会存在“超售问题”

完整示例:

juejin.cn/post/684490…

TCC事务

TCC(Try-Confirm-Cancel)是应用层面的两阶段提交,其执行流程:

第一阶段:Try,尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需用到的业务资源(保障隔离性)。比如下单的try阶段对库存标记为占用。

第二阶段:根据第一阶段的结果决定是执行confirm还是cancel

  • Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。要求具备幂等设计,Confirm失败后需要进行重试。
  • Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。要求具备幂等设计,Cancel失败后需要进行重试。

TCC方案的优缺点

(1)优点

  • 具备较强的隔离性,避免“超售问题”

  • 相对基础设施层面的2PC来说,可以在代码层面根据需要设计资源锁定的粒度和方式,减少锁的竞争,提升性能。

  • 相比2PC来说,不依赖数据库,能够实现跨数据库、跨应用资源管理

(2)缺点

  • TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度和开发成本较高

  • 参与者包含其它公司或遗留系统服务时,有可能无法提供 TCC 模式要求的三个接口

Saga事务

Saga事务模型又叫做长时间运行的事务,其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作

分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

适用场景:

  • 业务流程长、业务流程多

  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

Saga和TCC思路的主要差别是用数据补偿来代替回滚

可以使用状态机实现服务编排:

Saga事务的优缺点

(1)优点:

  • 一阶段提交本地数据库事务,无锁,高性能;

  • 参与者可以采用事务驱动异步执行,高吞吐;

  • 补偿服务即正向服务的“反向”,易于理解,易于实现;

(2)缺点:

  • 不能保证隔离性

  • 存在协调器单点故障风险,需要有机制保障故障后的继续提交或恢复问题 (SAGA Log)

分布式事务框架-seata

seata.io/zh-cn/docs/…

AT 模式

seata.io/zh-cn/docs/…

TCC 模式

seata.io/zh-cn/docs/…

seata.io/zh-cn/blog/…

Spring cloud集成TCC:

seata.io/zh-cn/blog/…

Saga 模式

seata.io/zh-cn/docs/…

缺乏隔离性的应对

  • 由于 Saga 事务不保证隔离性, 在极端情况下可能由于脏写无法完成回滚操作, 比如举一个极端的例子, 分布式事务内先给用户A充值, 然后给用户B扣减余额, 如果在给A用户充值成功, 在事务提交以前, A用户把余额消费掉了, 如果事务发生回滚, 这时则没有办法进行补偿了。这就是缺乏隔离性造成的典型的问题, 实践中一般的应对方法是:
    • 业务流程设计时遵循“宁可长款, 不可短款”的原则, 长款意思是客户少了钱机构多了钱, 以机构信誉可以给客户退款, 反之则是短款, 少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。

    • 有些业务场景可以允许让业务最终成功, 在回滚不了的情况下可以继续重试完成后面的流程, 所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力, 让业务最终执行成功, 达到最终一致性的目的。

XA 模式

seata.io/zh-cn/docs/…

分布式事务常见问题

1.幂等

所谓幂等性通俗的将就是一次请求和多次请求同一个资源产生相同的影响。用数学语言表达就是f(x)=f(f(x))

出现原因

接口调用可能会出现超时,实际上有可能已经处理成功,调用方重试时会出现重复请求问题。所以接口设计时要支持幂等性。

常用解决方案

  • 单号 + 状态 判断满足处理条件,配合分布式锁或读锁
  • 唯一事务ID ,判断是否处理过,配合分布式锁或读锁
  • 数据库唯一索引

2.允许空回滚

出现原因

try调用超时(丢包),触发cancel调用,导致未收到try,只收到cancel的请求。

解决方案 被调用方识别出未收到try请求,对cancel请求不处理。

3.防悬挂控制

出现原因

try调用超时(拥堵),触发cancel调用,try请求晚于cancel处理。

解决方案 被调用方要允许空回滚,等try请求到达后,判断如果已经cancel过,不处理try。

分布式事务的选择

  • 非必要尽量避免分布式事务

  • 合理的服务拆分方式可以降低分布式事务出现的场景

  • 同一个应用可以根据不同场景使用不同的分布式方案

各分布式事务方案对比

方案一致性隔离性性能开发成本适用场景开源组件
本地消息表最终一致性1.对隔离性无要求,允许小概率出现“超售问题”2.有对消息日志查看需求或无支持事务MQ环境
事务消息最终一致性1.对隔离性无要求,允许小概率出现“超售问题”2.具备支持事务MQ的环境RocketMQ
TCC最终一致性对隔离性有要求,需要防止“超售问题”seata
SAGA最终一致性1.对隔离性无要求,允许小概率出现“超售问题”2.无法使用TCC的场景3.长事务seata