Seata 是一款开源的分布式事务解决方案,AT 模式是 Seata 提供的一种无侵入的分布式事务解决方案,其实现原理主要涉及以下几个关键部分:
全局事务与分支事务
全局事务:在分布式系统中,一个业务操作通常会涉及多个微服务,Seata 会为整个业务操作创建一个全局事务,全局事务由全局事务 ID(XID)唯一标识。
分支事务:每个参与全局事务的微服务中的数据库操作都被视为一个分支事务,分支事务与全局事务关联,通过全局事务 ID 和分支事务 ID 来标识。
数据解析与日志记录
数据解析
SQL 解析:Seata 的 AT 模式会对执行的 SQL 语句进行解析,分析出 SQL 操作的类型(如 INSERT、UPDATE、DELETE)、操作的表结构以及相关的字段信息等。
数据快照:在执行 SQL 操作前,Seata 会根据解析结果获取数据的原始快照,记录数据在操作前的状态。同样,在 SQL 操作执行后,也会获取数据的当前快照,记录操作后的数据状态。
日志记录:Seata 会将数据的解析结果和快照信息记录到 undo_log 表中,undo_log 表是 Seata 用于存储回滚日志的表,每条记录对应一个分支事务的操作日志,包括全局事务 ID、分支事务 ID、数据的原始快照和当前快照等信息。
事务提交与回滚
事务提交
一阶段提交:在一阶段,Seata 的 AT 模式会先执行业务 SQL,然后将数据的变更记录到 undo_log 表中,最后将本地事务提交。此时,如果本地事务提交成功,说明业务数据的变更已经持久化到数据库中,但全局事务还未最终提交。
二阶段提交:当所有分支事务的一阶段都执行成功后,Seata 的全局事务协调器会向所有分支事务发起二阶段提交请求。分支事务收到提交请求后,会删除对应的 undo_log 记录,表示事务已经成功提交,无需回滚。
事务回滚
一阶段失败:如果在一阶段执行过程中,某个分支事务执行失败,Seata 的全局事务协调器会向所有分支事务发起回滚请求。
二阶段回滚:分支事务收到回滚请求后,会根据 undo_log 表中的数据快照进行数据回滚操作,将数据恢复到事务操作前的状态,然后删除 undo_log 记录,完成事务回滚。
分布式事务协调
全局事务协调器(TC):Seata 的全局事务协调器负责协调全局事务的执行,它会记录全局事务的状态、各个分支事务的状态等信息。在全局事务执行过程中,TC 会根据各个分支事务的执行结果来决定全局事务是提交还是回滚。
事务参与者(RM):在 Seata 的 AT 模式中,参与分布式事务的每个微服务中的数据库都作为事务参与者。事务参与者负责执行本地的数据库操作,并向全局事务协调器汇报事务的执行状态。
通信机制:Seata 的全局事务协调器与事务参与者之间通过网络通信进行交互,使用的通信协议通常是 TCP/IP。在通信过程中,会传递全局事务 ID、分支事务 ID、事务操作类型(提交、回滚等)等信息,以实现分布式事务的协调和控制。
基本使用:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count, int money) {
jdbcTemplateA.update("update stock_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
jdbcTemplateB.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
}
假设我们有一个user表,结构为
(id INT PRIMARY KEY, name VARCHAR(255), age INT),
执行一条INSERT语句
INSERT INTO user (id, name, age) VALUES (1001, '李四', 25),
在 Seata 的 AT 模式下,undo_log表中的记录可能如下:
{"branchId":10001,"xid":"xid_001","sqlType":"INSERT","tableName":"user","beforeImage":{"id":null,"name":null,"age":null},"afterImage":{"id":1001,"name":"李四","age":25}}
在这个例子中:
branch_id为10001,它是当前分支事务的唯一标识符。
xid为xid_001,代表全局事务 ID。
undo_log字段以 JSON 格式存储了详细的回滚日志信息
"sqlType":"INSERT"表明这是一个插入操作的日志。
"tableName":"user"指定了操作的表名。
"beforeImage"表示插入前的数据快照,由于是插入操作,这里的字段值都为null,表示插入前数据库中不存在这条记录。
"afterImage"表示插入后的数据快照,记录了实际插入到数据库中的数据值
回滚时执行的语句分析
在回滚过程中,Seata 会根据 undo_log 中的 afterImage 信息生成用于撤销插入操作的 SQL 语句。具体来说,会使用 afterImage 中的主键信息(如果有)来唯一标识要删除的记录。
对于上述例子,由于 id 是 user 表的主键,回滚时会执行如下 DELETE 语句:
DELETE FROM user WHERE id = 1001;
如果表没有主键,Seata 会尝试使用唯一索引或所有列的组合来尽可能准确地定位要删除的记录。例如,如果 user 表没有 id 作为主键,而是 name 和 age 组合起来具有唯一性
如果执行UPDATE语句
UPDATE user SET age = 30 WHERE id = 1
undo_log表会记录:
{"branchId":20001,"xid":"xid_002","sqlType":"UPDATE","tableName":"user","beforeImage":{"id": 1, "name": "张三", "age": 25},"afterImage":{"id": 1, "name": "张三", "age": 30}}
回滚:Seata 会根据 undo_log 表中的 beforeImage 信息生成反向的 UPDATE 语句,将数据恢复到更新前的状态
UPDATE user SET age = 25 WHERE id = 1;
综上,Seata 的AT模式和传统的2PC模式有一定区别,它没有采用数据库本身的回滚能力,而是通过undo_log生成反向语句来实现回滚,这在一定程度上提升了性能。