分布式事务设计

本地事务

本文的重点不是讲解本地事务,但后面所讲的分布式事务又依赖本地事务提供的能力,所以这里还是需要简单提一下。

数据库事务的四个基本要素:ACID

  • 原子性(Atomicity)

  • 一致性(Consistency)

  • 隔离性(Isolation)

  • 持久性(Durability)

分布式事务

分类

  • 集群内的分布式事务
  • 异构环境下的分布式事务

通常,数据库集群内的分布式事务可以很好的工作,而异构环境下的分布式事务则更加复杂,本文主要讨论异构环境下的分布式事务。

XA 事务

XA 协议

​ 1991年,为了解决分布式事务的一致性问题,X/Open 组织提出了名为 XA 的事务处理接口,其核心内容是定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的双向通信接口。能在一个事务管理器和多个资源管理器之间互相通信。

​ XA 并不是Java的技术规范,而是一套语言无关的通用规范,所以Java中专门基于XA定义了全局事务处理标准-JTA。JTA 最主要的两个接口是事务管理器接口 TransactionManager 和 资源定义接口 XAResource。AtomikosNarayana(JBoss)是实现JTA接口的第三方组件。

二阶段提交

​ 二阶段提交是XA的标准实现。它将分布式事务的提交拆分为2个阶段:prepare 和 commit/rollback。

​ 开启XA全局事务后,所有子事务会按照本地默认的隔离级别锁定资源,并记录undo和redo日志,然后由TM发起prepare投票,询问所有的子事务是否可以进行提交:当所有子事务反馈的结果为“yes”时,TM再发起commit;若其中任何一个子事务反馈的结果为“no”,TM则发起rollback;如果在prepare阶段的反馈结果为yes,而commit的过程中出现宕机等异常时,则在节点服务重启后,可根据XA recover再次进行commit补偿,以保证数据的一致性。

​ 2PC模型中,在prepare阶段需要等待所有参与子事务的反馈,因此可能造成数据库资源锁定时间过长,不适合并发高以及子事务生命周长较长的业务场景。

缺点

两阶段提交原理简单,并不难实现,但有几个非常显著的缺点:

  1. 单点问题
  2. 性能问题
  3. 数据一致问题

​ 2PC中的协调者和参与者,通常都是由数据库自己来扮演的。一旦宕机的不是其中一个参与者,而是协调者的话,那么所有参与者都将受到影响;

三阶段提交

三阶段提交在单点问题和回滚时的性能问题上有所改善,但并没有解决一致性问题,所以三阶段提交的实际应用并不多。

可靠事件队列

​ 这种靠着不断重试来保证可靠性的解决方案,在计算机的其他领域中已被频繁使用,也有了专门的名字叫作最大努力交付

RocketMQ 原生支持分布式事务操作。

总结

关键词:不断重试,已达到最大努力交付的目的。

缺点:缺乏事务隔离性,可能产生“超卖”。

TCC 事务

​ TCC 是另一种常见的分布式事务机制,它是“Try-Confirm-Cancel”三个单词的缩写,是由数据库专家 Pat Helland 在 2007 年撰写的论文《Life beyond Distributed Transactions: An Apostate’s Opinion》中提出。

​ 前面介绍的可靠消息队列虽然能保证最终的结果是相对可靠的,过程也足够简单(相对于 TCC 来说),但整个过程完全没有任何隔离性可言,有一些业务中隔离性是无关紧要的,但有一些业务中缺乏隔离性就会带来许多麻烦。譬如在本章的场景事例中,缺乏隔离性会带来的一个显而易见的问题便是“超售”:完全有可能两个客户在短时间内都成功购买了同一件商品,而且他们各自购买的数量都不超过目前的库存,但他们购买的数量之和却超过了库存。如果这件事情处于刚性事务,且隔离级别足够的情况下是可以完全避免的

​ 在具体实现上,TCC 较为烦琐,它是一种业务侵入式较强的事务方案,要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。如同 TCC 的名字所示,它分为以下三个阶段。

  • Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需用到的业务资源(保障隔离性)。
  • Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。Confirm 阶段可能会重复执行,因此本阶段所执行的操作需要具备幂等性。
  • Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。Cancel 阶段可能会重复执行,也需要满足幂等性。

总结

​ TCC 其实有点类似 2PC 的准备阶段和提交阶段,但 TCC 是位于用户代码层面,而不是在基础设施层面,这为它的实现带来了较高的灵活性,可以根据需要设计资源锁定的粒度。TCC 在业务执行时只操作预留资源,几乎不会涉及锁和资源的争用,具有很高的性能潜力。但是 TCC 并非纯粹只有好处,它也带来了更高的开发成本和业务侵入性,意味着有更高的开发成本和更换事务实现方案的替换成本,所以,通常我们并不会完全靠裸编码来实现 TCC,而是基于某些分布式事务中间件(比如阿里开源的Seata)去完成,尽量减轻一些编码工作量。

关键词:

​ 预留业务资源,保证隔离性;性能较好;业务侵入较强,开发成本高;

优点:

​ TCC 事务具有较强的隔离性,避免了“超售”的问题,而且其性能一般来说是本篇提及的几种柔性事务模式中最高的。

缺点:

​ TCC 的最主要限制是它的业务侵入性很强,开发和维护成本高。

SAGA 事务

​ SAGA 事务模式的历史十分悠久,还早于分布式事务概念的提出。它源于 1987 年普林斯顿大学的 Hector Garcia-Molina 和 Kenneth Salem 在 ACM 发表的一篇论文《SAGAS》(这就是论文的全名)。文中提出了一种提升“长时间事务”(Long Lived Transaction)运作效率的方法,大致思路是把一个大事务分解为可以交错运行的一系列子事务集合。原本 SAGA 的目的是避免大事务长时间锁定数据库的资源,后来才发展成将一个分布式环境中的大事务分解为一系列本地事务的设计模式。

SAGA 由两部分操作组成:

  1. 大事务拆分若干个小事务
  2. 为每一个子事务设计对应的补偿动作

如果 T1到 Tn均成功提交,那事务顺利完成,否则,要采取以下两种恢复策略之一:

  • 正向恢复(提交重试,最大努力交付)
  • 反向恢复(补偿重试,最大努力交付)

​ SAGA 必须保证所有子事务都得以提交或者补偿,但 SAGA 系统本身也有可能会崩溃,所以它必须设计成与数据库类似的日志机制(被称为 SAGA Log)以保证系统恢复后可以追踪到子事务的执行情况。

总结

关键词:

​ 专为长事务设计,为每个子事务设计补偿动作,如果提交失败,提供正向恢复和反向恢复两种策略。

AT 模式

​ 从整体上看是 AT 事务是参照了 XA 两段提交协议实现的,针对 XA 2PC 的缺陷(即在准备阶段必须等待所有数据源都返回成功后,协调者才能统一发出 Commit 命令而导致的木桶效应),设计了针对性的解决方案。大致的做法是在业务数据提交时自动拦截所有 SQL,将 SQL 对数据修改前、修改后的结果分别保存快照,生成行锁,通过本地事务一起提交到操作的数据源中,相当于自动记录了重做和回滚日志。如果分布式事务成功提交,那后续清理每个数据源中对应的日志数据即可;如果分布式事务需要回滚,就根据日志数据自动产生用于补偿的“逆向 SQL”。基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。这种异步提交的模式,相比起 2PC 极大地提升了系统的吞吐量水平。

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

​ 提升了系统的吞吐量的代价就是大幅度地牺牲了隔离性,甚至直接影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚并不一定是总能成功的。譬如,当本地事务提交之后、分布式事务完成之前,该数据被补偿之前又被其他操作修改过,即出现了脏写,这时候一旦出现分布式事务需要回滚,就不可能再通过自动的逆向 SQL 来实现补偿,只能由人工介入处理了。脏写情况一旦发生,人工其实也很难进行有效处理。为了防止脏写情况的发生,Seata 在AT模式中进入全局锁机制。

写隔离

步骤如下:

  • 一阶段开启本地事务进行数据操作,但提交前,必须确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

​ tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

​ tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

​ 如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离

​ 如果数据库本地事务隔离级别为读已提交或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交 。这意味着可能产生脏读。

​ 如果应用在特定场景下,必需要求全局 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

​ SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁被其他事务持有,则释放本地锁并重试。这个过程中,查询是被阻塞住的,直到 全局锁 拿到,即读取的相关数据是已提交的。

​ 阻塞读取的代价还是非常大的,出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

由此可见,分布式事务中没有一揽子包治百病的解决办法,因地制宜选用合适的事务处理方案才是唯一有效的做法。

解决方案 描述 优点 缺点
XA 基于2PC实现的一种强一致性事务,将事务提交分为两个阶段:准备和执行,协调者必须等待所有的参与者准备就绪,才能统一执行,一旦全员同意执行,则只允许成功,如果出现宕机,恢复后应该自动提交。 数据强一致性,具有很好的隔离性 协调者是数据库自己扮演的,如果协调者出现单点故障,则整个事务将会停止;
在极端情况下会出现数据不一致的问题;
性能问题;
可靠事件队列 依靠不断重试来保证可靠性的解决方案,保证最大努力交付。每种操作要考虑具备幂等性。 实现简单,可以保证结果最终一致性 失去了隔离性,可能出现超卖问题。
TCC 要求业务处理过程必须拆分为“预留业务资源”和 “确认/释放消费资源”两个子过程。预留业务资源的目的是保障隔离性,在所有子任务全部预留好之前,有一个出现问题则全部取消进行资源释放;一旦全部预留好资源,进入Confirm阶段,则以不断重试的方式保证最大努力交付。 具有很好的隔离性;性能不错; 实现繁琐,对业务侵入性较强,开发成本高
SAGA 不保证隔离性
AT 在业务数据提交时,自动生成重做/回滚日志到本地数据表,如果分布式事务成功提交,那后续清理每个数据源中对应的日志数据即可;如果需要回滚,就根据自动产生用于补偿的“逆向 SQL”。基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。 提交异步化,提升系统吞吐量 牺牲隔离性

Seata 服务

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

相关术语

TC (Transaction Coordinator) - 事务协调者

​ 维护全局和分支事务的状态,驱动全局事务提交或回滚。通常由第三方服务(Seata)实现。

TM (Transaction Manager) - 事务管理器

​ 定义全局事务的范围:开始全局事务、提交或回滚全局事务。通常指应用程序。

RM (Resource Manager) - 资源管理器

​ 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。通常由数据库实现。

事务模式

  • XA 模式
  • AT模式
  • TCC 模式
  • SAGA 模式

工作原理

Seata 由三部分组成:

  • TC 事务 seata服务器
  • TM 发起分布式事务
  • RM 资源(每个微服务的数据库)

部署Seata 服务

​ 在使用Seata 之前需要进行部署安装以及一些准备工作。官网下载 Seata 安装包,安装后在安装目录找到README文件,里面有详细的准备说明,Seata 支持多种方式持久化数据,推荐使用 db 数据库方式,Seata 服务的安装使用步骤如下:

Serve 配置:

  1. 官网下载安装Seata;
  2. 修改 Seata 安装目录conf 下的 file.conf ,配置事务日志持久化方案;推荐 db 方式,持久化到数据库;
  3. 修改conf 目录下的 register.conf, 配置 注册中心和配置中心;推荐 nacos;
  4. 新建 seata 数据库, 运行初始化SQL脚本.(官方会给, 新建3张表: branch_table, global_table, lock_table) ;

Client 配置:

  1. 在每个微服务的数据库中运行初始化SQL脚本.(官方会给, 新建1张表: undo_log)
  2. 在微服务项目的resources 目录下新建 file.confregister.conf 配置文件 (复制官方的demo并修改);
  3. 配置 application.yaml ,新增 spring.cloud.alibaba.seata.tx-service-group 的事务名称, 该事务名称与 file.conf 中的事务名称要一致;
  4. pom.xml 中引入 seata 相关依赖;
  5. 在相应的Service 方法上标注 @GlobalTransactional 注解;

注:先启动注册中心 Nacos, 再启动 Seata 服务, Seata 服务默认使用 8091 端口号启动;

README

作者:银法王

参考:

​ 《凤凰架构》周志明

从本地事务到分布式事务

分布式事务,这一篇就够了【小米信息部技术团队】

修改记录:

 2022-11-08 第一次修订


分布式事务设计
http://jackpot-lang.online/2023/04/08/系统技术架构设计/分布式系列-分布式事务设计/
作者
Jackpot
发布于
2023年4月8日
许可协议