分布式事务

分布式事务是几乎每一个分布式系统中都会涉及到的一个技术问题,特别如今微服务架构被广泛应用在各企业的开发场景中。本文对分布式事务相关学习进行整理归纳,做为后续学习参考,其间参考和引用了部分网络和书籍资料,都很有借鉴价值,特在每篇文章末尾的参考资料环节中附注。

本地事务

在过去很多年的开发过程中,基于数据库的事务一直被广大开发人员和数据库管理与使用人员所熟知。广义上事务是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元,狭义上事务在某种情况下特指数据库事务。

ACID

事务具有四个特征,分别是原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durabilily),简称事务的 ACID 属性。

原子性 (Atomicity): 指事务必须是一个原子的操作序列单元,事务中包含的各项操作在一次执行过程中,要么全部执行成功,要么全部执行失败;

一致性 (Consistency): 指事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。

隔离性 (Isolation): 指多个事务并发执行的时候,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(serializable)。

持久性 (Durabilily): 指事务一旦提交,它对数据库中对应数据的状态变更应该是永久的,后续其他操作或故障不应该对其有任何影响。

隔离级别

标准 SQL 规范中定义了 4 个事务的隔离级别,不同的隔离级别对事务的处理相应不同。分别为:读未提交 (Read uncommitted)、读已提交 (Read committed)、可重复读 (Repeatable read) 和串行化 (Serializable)。

读未提交:最低的隔离级别。允许 “脏读” (Dirty Reads),事务可以看到其他事务 “尚未提交” 的修改。

读已提交: 基于锁机制并发控制的 DBMS 需要对选定对象的写锁一直保持到事务结束,但是读锁在 SELECT 操作完成后马上释放。解决了 “脏读” 的问题,但是会存在 “不可重复读” 的问题 (Nonrepeatable Reads)。

可重复读:基于锁机制并发控制的 DBMS 需要对选定对象的读锁和写锁一直保持到事务结束,但不要求“范围锁”。解决了 “不可重复读” 的问题,但是可能会发生 “幻读” (Phantoms)。

串行化:在基于锁机制并发控制的 DBMS 实现可串行化,要求在选定对象上的读锁和写锁保持直到事务结束后才能释放。可以避免 “幻读”(Phantoms)现象。

对于其中涉及的几种读现象,解释如下:

脏读:当一个事务允许读取另外一个事务修改但未提交的数据时,就可能发生脏读。

dirty-read

事务 T1 首先从表中查询 id=1 的字段 (value = a),然后另外一个事务 T2 更新表中 id=1 的字段 (value = b), 但是此时 T2 未提交事务,T1查询到的为事务 T2 更新的值 b,后事务 T2 回滚事务,T1 查询到的还是 T2 更新的值 b,发生数据不一致。

不可重复读

nonrepeatable-reads

事务 T1 首先从表中查询 id=1 的行,然后另外一个事务 T2 更新表中 id=1 的字段值为 b 并且提交事务, 这时 T1 看到的仍然是它之前查询到的结果,在此执行 select 是仍然和之前的查询结果一样。

幻读

phantoms-read

事务 T1 首先从表中查询 id 从 1 到 20 的行,然后另外一个事务 T2 往表插入了 id=3 的行并且提交事务, 这时 T1 看到的结果集仍然和它之前查询到的结果集一样。

关于事务的隔离级别,可以归结为下表:

隔离级别 脏读 不可重复读 幻读
读未提交 Y Y Y
读已提交 N Y Y
可重复读 N N Y
串行化 N N N

分布式理论

在单机数据库中,实现一套满足 ACID 事务特性的事务处理系统相对比较容易,但是在分布式场景下,数据分散在不同的机器上,如何对这些数据进行分布式的事务处理具有非常大的挑战。分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上,通过一个分布式事务,往往会涉及对多个数据源或业务系统的操作。

于是,伴随着分布式系统和分布式理论的发展,出现了 CAP 和 BASE 这样的分布式系统经典理论。CAP 理论指分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition tolerance),最多只能同时满足其中两项。相对应的,BASE 理论是基本可用 (Basically Available)、软状态 (Soft state) 和最终一致性 (Eventually consistent) 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。

有关 CAP 和 BASE 理论的讨论可以参见之前的文章 分布式系统CAP理论和BASE思想概述, 这里不做过多描述。

分布式事务

在分布式系统中,实现分布式事务主要可以通过以下几种方式:

两阶段提交

两阶段提交协议也被认为是一种一致性协议,在分布式场景下,能够方便的完成所有分布式事务参与者的协调,统一决定事务的提交或混滚,从而有效保证事务的分布式数据一致性。

有关两阶段提交的讨论可以参考之前的文章 两阶段协议

三阶段提交

三阶段提交协议是两阶段提交协议的改进版,将两阶段提交协议的“提交阶段”进行拆分,形成了由 CanCommit、PreCommit 和 doCommit 三个阶段组成的事务处理协议。

有关两阶段提交的讨论可以参考之前的文章 三阶段协议

TCC

TCC (Try-Confirm-Cancel) 分布式模型相较于 XA 等基于 2PC 实现的事务模型,特征在于不依赖于资源管理器对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。

TCC 模型认为对于业务系统中的一个特定的业务逻辑,其对外提供服务时,必须接受一些不确定性,即对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业务服务认为全局事务应该回滚,它会要求取消之前的临时操作,这就对应从业务服务的取消操作。而当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认操作。每一个初步操作,最终都会被确认或取消。

因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑:

  1. 初步操作 Try: 完成所有业务检查,预留必须的资源;
  2. 确认操作 Confirm: 真正执行业务逻辑,不做任务业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 成功,Confirm 必须能成功。另外,Confirm操作需满足幂等性,保证每一笔分布式事务有且只能成功一次。
  3. 取消操作 Cancel: 释放 Try 阶段预留的业务资源。同样,Cancel 操作也需要保证幂等性。 tcc-transaction-model

TCC 模型包括三部分:

  1. 主业务服务:整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动;
  2. 从业务服务:整个业务活动的参与方,负责提供 TCC 业务操作,实现初步操作 (Try)、确认操作 (Confirm)、取消操作 (Cancel)三个接口,供主业务服务调用;
  3. 业务活动管理器:管理控制整个业务活动,包括记录维护 TCC 全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的 Confirm操作, 在业务活动取消时调用所有从业务服务的 Cancel 操作。

TCC 模型流程如下:

  1. 主业务服务开启本地事务;
  2. 主业务服务向业务活动管理器申请启动分布式事务主业务活动;
  3. 针对要调用的从业务服务,主业务活动先向活动管理器注册从业务活动,然后调用从业务活动的 Try 接口;
  4. 当所有从业务服务的 Try 接口调用成功,主业务服务提交本地请求;若调用失败,主业务服务回滚本地事务;
  5. 若从业务服务提交本地事务,则 TCC 模型分别调用所有从业务服务的 Confirm 接口;若主业务服务回滚本地事务,则分别调用 Cancel 接口;
  6. 所有从业务服务的 Confirm 或 Cancel 操作完成后,全局事务结束。

TCC 模型的原子性:TCC 模型可以看做 2PC 的一种变种,Try 操作对应 2PC 的一阶段 Prepare;Confirm 对应 2PC 的二阶段 Commit,Cancel 对应 2PC 的二阶段 Rollback,可以说 TCC 就是应用层的 2PC;

TCC 模型的隔离性:与 2PC 不同,2PC 要求一直持有锁直到第二阶段执行结束,会导致性能下降;TCC 模型的隔离性思想是通过业务的改造,在第一阶段结束之后,将从底层数据库资源层面的加锁过渡为上层业务层面的加锁,从而释放底层数据库锁资源,放宽分布式事务锁协议,提高业务并发性能。

TCC 模型的一致性:与 2PC 相同的是也是通过原子性保证事务的原子提交、业务隔离性控制事务的并发访问,实现分布式事务的一致性状态转变;但是在 TCC 模型中,事务的中间状态不能被观察到,会出现短暂的不一致,但是最终会达到一致性的状态,这符合 BASE 理论,也是柔性事务的提现。

以最常见的转账业务举例,假设用户张三向用户李四进行转账,这里把转账业务拆分为交易服务和账务服务两个分布式事务,交易服务作为主业务服务,账务服务作为从业务服务:

  1. 交易服务首先开启本地事务;
  2. 交易服务向业务活动管理器申请启动分布式事务主业务活动;
  3. 交易服务向活动管理器注册账务服务,调用账务服务的 Try 接口,对用户张三的资金进行冻结;
  4. 账务服务的 Try 接口调用成功,交易服务提交本地请求;
  5. 账务服务提交本地事务, 调用账务服务的 Confirm 接口扣除用户张三的预冻结资金,增加用户李四的可用资金;
  6. 全局事务结束

异步确保模型

异步确保模型也叫本地消息表模型,由 ebay 最早提出,后来在电商领域被大范围使用。基本思路是:

  1. 主业务服务增加消息记录表,维护待发送消息记录;从业务服务增加消息记录表,维护被处理的消息记录;其中主业务服务逻辑和主业务服务待发送消息记录在一个事务,从业务服务逻辑和从业务服务被处理的消息在一个事务;
  2. 主业务服务处理业务逻辑,并向待发送消息记录表增加消息记录,主业务逻辑和消息记录在一个事务;
  3. 主业务服务轮询消息记录表,发送消息到 MQ; 如果出现发送失败的情况,需要进行重试,所以从业务服务需保证幂等性;
  4. 从业务服务订阅 MQ 消息,向待处理消息记录表增加 messageId 记录;
  5. 从业务服务处理业务逻辑,处理成功后删除消息记录表中记录;向主业务服务发送 MQ 消息,通知主 MQ.
  6. 从业务轮询本地消息记录表,如果有未处理消息,则补处理后发送给主业务服务;

与 TCC 类似,异步确保模型也遵循 BASE 理论,不过在实现上更为简单,通过数据库和消息的方式即可实现。不过,相对而言对业务代码侵入性比较强,也可能存在一定的延时发生。

最大努力通知型

最大努力通知型主要也是借助 MQ 消息系统来进行事务控制,实现也比较简单,它本质上就是通过定期校对,实现数据一致性。基本思路是:

  1. 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。
  2. 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。
  3. 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
  4. 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
  5. 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。

最大努力通知型被动方的处理结果不影响主动方的处理结果,适用于对业务最终一致性的时间敏感度低的系统和适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作;

小结

以上是个人对分布式事务的一些总结,在分布式领域目前被应用最多的应该还是基于 2PC 的 XA 事务和 TCC 事务,而异步确保和最大努力通知型事务,通过 MQ 的消息传递来将大事务化解为本地小事务,方法也非常巧妙,最终还是取决于业务场景的选择。

参考资料

Base: An Acid Alternative

分布式事务概述

聊聊分布式事务,再说说解决方案

Isolation (database systems)

What is the difference between Non-Repeatable Read and Phantom Read?

一篇文章带你学习分布式事务-蚂蚁金服科技

聊分布式事务,再说说解决方案