一文详解分布式事务

765 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第31天,点击查看活动详情

事务是应用程序中一系列严密的操作,一个事务中一系列的操作要么全部都成功,要么全部都失败。事务具有 4 个属性:原子性、一致性、隔离性、持久性(ACID 特性)。 分布式事务是相对于单机事务而言的。在分布式场景下,一个系统由多个子系统构成,每个子系统有独立的数据源,当某一业务流程涉及到对于多个独立的数据源的更新操作时,保证业务操作的事务性就是我们常说的分布式事务问题。

1. 2PC 两阶段提交

两阶段提交的基础是XA 规范。XA 规范约束了事务管理器(TM)和资源管理器(RM)之间的交互,简单的说就是事务管理器和资源管理器之间要按照一定的格式规范来交流。应用程序 AP 通过 TM 来定义事务操作,TM 和 RM 之间会通过 XA 规范进行通信,执行两阶段提交。目前主流的数据库基本都支持XA 事务,包括 mysql、oracle、sqlserver、postgre。

2PC 的具体流程

  • 阶段一: 准备阶段

事务协调者向事务参与者发送Prepare()请求,询问各个事务参与者是否就绪,每个事务参与者写入本地事务日志,并向事务协调者汇报各自的准备状态,成功还是失败。

  • 阶段二: 提交阶段

协调者基于各个事务参与者的准备状态,来决策是事务提交Commit()或事务回滚Rollback()

image.png

2PC 事务特点:

  • 原子性:在准备和提交/回滚阶段保证事务是原子性的。

  • 一致性:XA 协议实现了强一致性。

  • 隔离性:由每个 RM 本地事务的隔离性来保证。

  • 持久性:基于每个本地事务保证。

优点:

  • 对业务侵⼊小,简单易理解,开发较容易。

  • 可以像使⽤本地事务⼀样使⽤基于 XA 协议的分布式事务,能够严格保障事务 ACID 特性。

缺点:

  • 同步阻塞:当事务参与者存在占用公共资源的情况时,其他事务参与者就只能阻塞等待资源释放,可能存在并发性能问题。

  • 单点故障:一旦事务管理器出现故障,整个系统将不可用。

  • 数据不一致:在第二阶段如果事务管理器因网络发生异常只成功发送部分 commit 命令,那么只有部分参与者接收到 commit 命令,即只有部分参与者提交了事务,最终系统数据不一致。

2. 3PC 三阶段提交

相比于 2PC,3PC 新增了一个阶段(准备阶段,与 2PC 的准备阶段不同)使得参与者可以利用该阶段统一各自的状态,以解决 2PC 同步阻塞和减少数据不一致的情况。

3PC 包括三个阶段,准备、预提交和提交:

image.png

另外, 3PC 在参与者中引入了超时机制,在协调者挂了的情况下,如果已经到了提交阶段了,参与者超过一定时间没收到协调者命令的话就会自动提交事务。

但是,多了一次交互通信过程,绝大部分情况下可能都是无用通讯,可能导致性能更差。另外也不能完全避免数据不一致的问题。

3. TCC

不管是 2PC 还是 3PC,都是数据库层面的事务。但是有些需求需要我们在业务层面保障分布式事务的一致性,这时就要提到TCC 模型了。

TCC本质上是一个业务层面上的2PC,他要求业务在使用TCC模式时必须实现三个接口Try()、Confirm()和Cancel()。

TCC 具体流程

  • 阶段一: 准备阶段

事务协调者向事务参与者发送Try()请求,要求各个事务参与者检查并锁定/冻结各自的资源,并向事务协调者汇报各自的锁定状态,成功还是失败。

  • 阶段二: 提交阶段

协调者基于各个事务参与者的锁定状态,来决策是事务执行Confirm()或事务取消Cancel(),如果Confirm()或Cancel()调用失败,协调者就会不断重试,所以Confirm()和Cancel()必须是幂等的,当然这个重试在具体实现时可以是一个异步的任务。

在阶段一Try()只是锁定了资源,真正执行业务逻辑是在Confirm()中,所以Try+Confirm对应一个完全的业务逻辑,在业务实现时,需要着重去考虑如何将一个业务逻辑拆分成Try和Confirm两个操作。

image.png

TCC 有以下几个设计要点

  • 空回滚

如果协调者的Try()请求因为网络超时失败,那么协调者在阶段二时会发送Cancel()请求,而这时这个事务参与者实际上之前并没有执行Try()操作而直接收到了Cancel()请求。

针对这个问题,TCC模式要求在这种情况下Cancel()能直接返回成功,也就是要允许「空回滚」。

  • 防悬挂

接着上面的问题1,Try()请求超时,事务参与者收到Cancel()请求而执行了空回滚,但就在这之后网络恢复正常,事务参与者又收到了这个Try()请求,所以Try()和Cancel()发生了悬挂,也就是先执行了Cancel()后又执行了Try()

针对这个问题,TCC模式要求在这种情况下,事务参与者要记录下Cancel()的事务ID,当发现Try()的事务ID已经被回滚,则直接忽略掉该请求。

  • 幂等性

Confirm()和Cancel()的实现必须是幂等的。当这两个操作执行失败时协调者都会发起重试。

4. Saga模式

Saga模式的核心思想是将长事务(Long Live Transaction)拆分为多个本地短事务,由 Saga 事务协调器负责按照一定顺序执行事务链中的分支事务,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序依次调用补偿操作。

image.png

如上图所示,一个分布式事务包含多个事务参与者,他们的执行操作/正向操作用T1 ~ Tn表示,Saga模式要求每个事务参与者提供一个补偿操作/逆向操作,用C1 ~ Cn表示,并且执行操作和补偿操作都是幂等的,如果事务参与者执行成功就正常提交,如果参与者执行失败,会按照拓扑逆序分别进行补偿操作,但是这个补偿是一种不完全补偿,因为每个本地事务都已经被提交,所以这个补偿操作无法保证每个事务都能被回滚,但是Saga模式的补偿操作其实是业务层面的一种回退,比如说补偿操作可以是给用户发一个短信,说明这次交易被取消请重新提交,也可以是把订单状态修改为已取消。

Saga模式 有以下几个设计要点

  • 空补偿

同TCC模式,当遇到网络问题时,事务参与者可能会只收到补偿操作的请求,所以补偿操作要求能「空补偿」。

  • 防悬挂

同TCC模式,当遇到网络问题时,事务参与者可能会先于执行操作收到补偿操作的请求,所以执行操作要求能「防悬挂」。

  • 幂等性

执行操作和补偿操作的实现必须是幂等的。当这两个操作执行失败时协调者都会发起重试。

因为正常情况下每个事务参与者都会直接提交,而不需要等待其他参与者的状态,所以Saga模式的并发性能非常好。

但是他的隔离性比较差,他的隔离级别是读未提交,这就意味着一个事务可以读到其他事务还没未提交的数据状态。

对于业务的接入成本,只需要为Saga单独实现一个补偿操作,而对于补偿操作的实现要求也不是很高,所以Saga的接入成本相对来说比较低。

综上,Saga模式比较适合于对并发性能和业务接入成本要求比较高,但是对于有隔离性要求不高的场景,如果有遗留系统要接入,考虑到Saga的接入成本比较低,所以这时也适合Saga模式。