单体应用可以依赖数据库事务,将一系列操作限制在一个会话之中同时成功或失败,但是在分布式系统中,每个服务本身的数据库会话之间是隔离的,就无法单纯的依赖数据库事务,在这种场景下想要保证数据的一致性,就需要引入分布式事务。
本文主要介绍Seate这个分布式事务组件,对并其服务模式进行简要介绍。
正文
术语
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
AT 模式
AT 模式同样是分阶段提交的事务模型,不过弥补了XA模型中==资源锁定周期过长==的缺陷。
实现流程:
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
如何防止多线程并发写造成的回滚异常
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 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 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
Seata的隔离级别与Seata分布式事务之间的可见性有关,与数据库事务的隔离无关。 这里分布式事务的隔离级别说的是分布式事务和分布式事务之间的可见性,上升了一个层面不要把它想成单机事务的隔离性。Seata的隔离级别默认读未提交,可以支持读已提交。全局事务的隔离级别一定基础上依赖本地事务的隔离级别。因此本地事务的隔离级别只要大于等于Seata支持的隔离级别就行,所以一般数据库的默认级别就可以。
TCC(补偿服务)
TCC 是 Try、Confirm、Cancel 三个词的缩写,它分为三个阶段:
- Try(尝试阶段) : 尝试执行。完成业务检查,并预留好必需的业务资源。
- Confirm(确认)阶段 :确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel 。
- Cancel(取消)阶段 :取消执行,释放 Try 阶段预留的业务资源。
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
Sagas
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
其核心思想是「将长事务拆分为多个本地短事务」,由Saga事务协调器协调,如果正常结束那就正常完成,如果「某个步骤失败,则根据相反顺序一次调用补偿操作」。
适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性
XA模式
这里有两个基本的前置概念:
- 什么是 XA?
- 什么是 Seata 定义的所谓 事务模式?
什么是 XA?
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。
XA 规范 描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范 的目的是允许多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。
XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。
XA 规范 在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范 提供了支持。
什么是 Seata 的事务模式?
Seata 定义了全局事务的框架。
全局事务 定义为若干 分支事务 的整体协调:
- TM 向 TC 请求发起(Begin)、提交(Commit)、回滚(Rollback)全局事务。
- TM 把代表全局事务的 XID 绑定到分支事务上 。
- RM 向 TC 注册,把分支事务关联到 XID 代表的全局事务中。
- RM 把分支事务的执行结果上报给 TC。(可选)
- TC 发送分支提交(Branch Commit)或分支回滚(Branch Rollback)命令给 RM。
Seata 的 全局事务 处理过程,分为两个阶段:
- 执行阶段 :执行 分支事务,并 保证 执行结果满足是 可回滚的(Rollbackable) 和 持久化的(Durable)。
- 完成阶段: 根据 执行阶段 结果形成的决议,应用通过 TM 发出的全局提交或回滚的请求给 TC,TC 命令 RM 驱动 分支事务 进行 Commit 或 Rollback。
Seata 的所谓 事务模式 是指:运行在 Seata 全局事务框架下的 分支事务 的行为模式。准确地讲,应该叫作 分支事务模式。
不同的 事务模式 区别在于 分支事务 使用不同的方式达到全局事务两个阶段的目标。即,回答以下两个问题:
- 执行阶段 :如何执行并 保证 执行结果满足是 可回滚的(Rollbackable) 和 持久化的(Durable)。
- 完成阶段: 收到 TC 的命令后,如何做到分支的提交或回滚?
以我们 Seata 的 AT 模式和 TCC 模式为例来理解:
AT 模式
- 执行阶段:
- 可回滚:根据 SQL 解析结果,记录回滚日志
- 持久化:回滚日志和业务 SQL 在同一个本地事务中提交到数据库
- 完成阶段:
- 分支提交:异步删除回滚日志记录
- 分支回滚:依据回滚日志进行反向补偿更新
TCC 模式
执行阶段:
- 调用业务定义的 Try 方法(完全由业务层面保证 可回滚 和 持久化)
完成阶段:
分支提交:调用各事务分支定义的 Confirm 方法
分支回滚:调用各事务分支定义的 Cancel 方法
什么是 Seata 的 XA 模式?
XA 模式:
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
- 执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
为什么支持 XA?
补偿型事务模式的问题
本质上,Seata 已经支持的 3 大事务模式:AT、TCC、Saga 都是 补偿型 的。
补偿型 事务处理机制构建在 事务资源 之上(要么在中间件层面,要么在应用层面),==事务资源 本身对分布式事务是无感知的==。
事务资源 对分布式事务的无感知存在一个根本性的问题:无法做到真正的 全局一致性 。
比如,一条库存记录,处在 补偿型 事务处理过程中,由 100 扣减为 50。此时,仓库管理员连接数据库,查询统计库存,就看到当前的 50。之后,事务因为异外回滚,库存会被补偿回滚为 100。显然,仓库管理员查询统计到的 50 就是 脏数据。
可以看到,补偿型分布式事务机制因为不要求事务资源 本身(如数据库)的机制参与,所以无法保证从事务框架之外的全局视角的数据一致性。
XA 的价值
与 补偿型 不同,XA 协议 要求 事务资源 本身提供对规范和协议的支持。
因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。
比如,上一节提到的库存更新场景,XA 事务处理过程中,中间态数据库存 50 由数据库本身保证,是不会仓库管理员的查询统计 看到的。(当然全局隔离级别需要 读已提交 以上)
除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处:
- 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
- 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。
- 多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少,为不同语言开发 SDK 较之 AT 模式将更 薄,更容易。
- 传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。
XA 广泛被质疑的问题
- 数据锁定:数据在整个事务处理过程结束前,都被锁定,读写都按隔离级别的定义约束起来。
思考:
数据锁定是获得更高隔离性和全局一致性所要付出的代价。
补偿型 的事务处理机制,在 执行阶段 即完成分支(本地)事务的提交,(资源层面)不锁定数据。而这是以牺牲 隔离性 为代价的。
另外,AT 模式使用 全局锁 保障基本的 写隔离,实际上也是锁定数据的,只不过锁在 TC 侧集中管理,解锁效率高且没有阻塞的问题。
- 协议阻塞:XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。
思考:
协议的阻塞机制本身并不是问题,关键问题在于 协议阻塞 遇上 数据锁定。
如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。
这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。
基本思路是两个方面:避免 “失联” 和 增加 “自解锁” 机制。(这里涉及非常多技术细节,暂时不展开,在后续 XA 模式演进过程中,会专门拿出来讨论)
- 性能差:性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。
思考:
和不使用分布式事务支持的运行场景比较,性能肯定是下降的,这点毫无疑问。
本质上,事务(无论是本地事务还是分布式事务)机制就是拿部分 性能的牺牲 ,换来 编程模型的简单 。
与同为 业务无侵入 的 AT 模式比较:
首先,因为同样运行在 Seata 定义的分布式事务框架下,XA 模式并没有产生更多事务协调的通信开销。
其次,并发事务间,如果数据存在热点,产生锁冲突,这种情况,在 AT 模式(默认使用全局锁)下同样存在的。
所以,在影响性能的两个主要方面,XA 模式并不比 AT 模式有非常明显的劣势。
AT 模式性能优势主要在于:集中管理全局数据锁,锁的释放不需要 RM 参与,释放锁非常快;另外,全局提交的事务,完成阶段异步化。
总结
XA 模式的加入,补齐了 Seata 在全局一致性 场景下的缺口,形成 AT、TCC、Saga、XA 四大事务模式 的版图,基本可以满足所有场景的分布式事务处理诉求。
文档信息
- 本文作者:L1Chenxv
- 本文链接:https://l1chenxv.github.io//2023/09/21/Seate/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)