分布式事务讲解 - Seata分布式事务框架(AT、TCC两种模式)
分布式事务系列博客:
【分布式事务讲解 - 2PC、3PC】
【分布式事务讲解 - TX-LNC分布式事务框架(含LCN、TCC、TXC三种模式)】
Seata分布式事务框架在公司中使用特别广泛,其中含有四种分布式事务模式:AT、TCC、SAGA、XA,其实与TX-LCN框架一样,都可以说是2PC分布式事务模型的一个变种,工作原理也都相似,值得一说的是,Seata框架的AT模式有对回滚数据的保存,但是TX-LCN这种框架现在还没有做到这么智能,所以现在公司大多是用Seata框架。
本篇博客内容太多,大家可先看理论部分,代码实战部分可以直接用最后分享的源码在本地跑一下,然后了解简单使用即可。
Seata原理
官方文档地址
这篇博客来源于本人在官网文档对这个框架解释的思考和总结,也看了很多其他人的博客。
http://seata.io/zh-cn/docs/overview/what-is-seata.html
Seata框架主要组成部分
Seata和TX-LCN框架的组成部分比较容易混淆,最好大家能理解这部分,区分这两个框架。
- TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。相当于TX-LCN的【TM】事务管理者。- TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。可以理解为【事务的发起者】。
在我看来,之所以Seata框架把【事务发起者】作为【事务管理器】,是因为,事务发起者有创建事务组和提交或回滚全局事务的能力。- RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,有向TC注册分支事务和报告分支事务的状态的能力,并驱动分支事务提交或回滚。
Seata工作流程
Seata原理图我就不展示了,可以说是与TX-LCN原理图一样的,大家有兴趣的话可以去看一下【分布式事务讲解 - TX-LNC分布式事务框架(含LCN、TCC、TXC三种模式)】这篇博客。
- 【事务管理器】发起事务,向【事务协调者】开启全局事务,并向【事务协调者】注册一个分支事务。
- 【事务管理器】调用【资源管理器A】,【资源管理器A】向【事务协调者】注册一个新的分支事务,【资源管理器A】执行业务,返回执行结果给【事务管理器】。
- 【资源管理器A】调用【资源管理器B】,【资源管理器B】向【事务协调者】注册一个新的分支事务,【资源管理器B】执行业务,返回执行结果给【事务管理器】。
- 等【事务管理器】调用的所有分支事务执行完毕并收到所有反馈信息,【事务管理器】会向【事务协调者】发送通知,表明分支事务全部执行完毕。
- 【事务协调者】根据【事务管理器】发送的分支事务执行情况,依次向所有【资源管理器】发出提交或回滚指令。
- 分布式事务结束。
全局事务和分支事务
Seata框架中把事务分成了全局事务和分支事务,正因为全局事务锁和分支事务锁的配合,解决了AT模式下的“脏读”和“脏写”问题。
全局事务
Seata框架有一个注解@GlobalTransactional,一个系统中加了这个注解的所有方法,属于一个共同的全局事务,这些方法执行时共同享有一个全局事务锁。同一个全局事务需要遵从AT模式的读写隔离机制。
分支事务(本地事务)
全局事务中会存在调用其他【资源管理器】的方法,每个方法就是一个分支事务,相同的方法属于同一个分支事务,同一个分支事务使用同一个分支事务锁。
AT模式(Automatic Transaction自动化事务)
这种事务模式使用起来比较简单,事务的回滚逻辑不用我们自行实现,我们只要实现分支事务逻辑即可,并且减少了数据库连接占用时长。
AT模式的工作流程是基于Seata框架流程的:
- 【事务管理器】发起事务,向【事务协调者】开启全局事务,并向【事务协调者】注册一个分支事务。
- 【事务管理器】调用【资源管理器A】,【资源管理器A】向【事务协调者】注册一个新的分支事务,【资源管理器A】执行本地事务SQL并提交事务以及保存回滚日志,释放本地占用的资源,返回执行结果给【事务管理器】。
- 【资源管理器A】调用【资源管理器B】,【资源管理器B】向【事务协调者】注册一个新的分支事务,【资源管理器B】执行本地事务SQL并提交事务以及保存回滚日志,释放对本地资源的占用,返回执行结果给【事务管理器】。
- 等【事务管理器】调用的所有分支事务执行完毕并收到所有反馈信息,【事务管理器】会向【事务协调者】发送通知,表明分支事务全部执行完毕。
- 【事务协调者】根据【事务管理器】发送的分支事务执行情况,依次向所有【资源管理器】发出提交或回滚指令,如果分支事务收到的是提交指令,那只把回滚日志删除即可;如果收到回滚命令,那就执行回滚日志并删除日志,如果发现当前数据和回滚日志中的修改后数据有差异,那就保留回滚日志等人工处理。
- 分布式事务结束。
由流程中可见:
- 在分支事务执行阶段就把sql提交,减少数据库连接占用时长,释放了本地事务的资源,提高了事务执行效率。
- 在分支事务执行阶段就保留回滚日志,使开发人员不用关心事务失败回滚的逻辑,事务失败,框架直接执行对应分支事务在对应回滚日志表中的记录进行事务回滚。
回滚日志具体内容
{
// 分支id
"branchId": 641789253,
"undoItems": [{
// 修改后信息
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
// 修改前信息
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
// 数据库表名
"tableName": "product"
},
// sql执行类型
"sqlType": "UPDATE"
}],
// 全局事务id
"xid": "xid:xxx"
}
回滚日志有对修改前和修改后数据的保存,用于回滚操作的自动进行,在执行回滚操作时,会根据分支事务id和全局事务id共同确定回滚日志,并且将回滚日志中的修改后信息与数据库当前数据信息进行对比,如果有差异,说明数据被修改过了,那就回滚失败,保留回滚日志,等候人工处理。
写隔离
AT模式为了防止产生“脏写”问题,加入了【写隔离】机制。
原理
- 两个不同线程(线程A、线程B)进入同一个分支事务执行相同业务,这时就要保证这两个线程不要出现脏写现象,这两个线程属于同一个全局事务。
- 线程A先拿到本地事务锁,执行业务,但是不进行事务提交。
- 线程A注册分支事务,拿到全局事务锁,提交本地事务。
- 线程A释放本地锁,但是还有其他业务未执行,全局事务锁依然没有释放。
- 本地事务锁释放后,线程B就可执行业务,执行完后不进行事务提交。
- 线程B注册分支事务,但是全局锁还被线程A占有,就会抛出异常,开启重试机制,如果重试超时依然没有拿到全局事务锁,那线程B就执行回滚操作,线程B整体事务结束。
- 线程A执行完全部业务,并全部提交成功,释放全局锁。
- 线程B重试获取全局锁成功,提交已执行的业务,等执行完全部业务,并全部提交成功,释放全局锁。
- 使用本地事务锁,才能减少对数据库连接的资源占用。
如果没有本地事务锁,那就会有多个线程占用数据库连接资源,数量达到阈值可能出现OOM。- 使用全局事务锁,才能保证对相同业务不被同时操作,才能保证整个分布式事务的提交和回滚操作正常进行。
如果没有全局事务锁,那就会出现线程A出错回滚事务时发现欲回滚数据已被线程B修改过的现象,导致无法正常回滚事务的后果。
读隔离
AT模式为了防止产生“脏读”问题,加入了【读隔离】机制。
原理
Seata代理了select for update,当要执行select for update语句时,会有以下流程:
- 获取本地事务锁,执行select for update。
- 如果方法被@GlobalTransactional注解修饰,那就检查全局事务锁是否能拿到。
- 如果全局事务锁被占用,那就回滚本地事务,重试回去本地事务锁和全局锁,直到全局锁释放并被拿到为止。
由原理可以看出,使用select for update进行查询的时候,会像写隔离机制一样也会等到全局事务锁的释放后才能进行查询,这种方式也就避免了脏读的情况。
官网语录解析
官网说过:【AT模式 基于 支持本地 ACID 事务
的 关系型数据库】,那原因是什么呢?这个我想了很久,网上的和官网也都有做过简单描述,但是描述的都一笔带过,让原本就不懂的人还是不懂,我是这样认为的:
- AT模式是基于支持本地事务的关系型数据库。
- AT模式要实现【读隔离】和【写隔离】,就一定需要支持不能自动提交执行sql的数据库,因为,这两种隔离原理就是通过拿到全局事务锁才能进行事务提交来实现的,但是不支持本地事务的数据库执行完sql就自动提交了,无法适用于这两种隔离,也就无法解决【脏读】和【脏写】问题。
所以,根据以上两点,我认为使用AT模式的分布式事务数据库必须在本地支持事务。
代码演示
创建数据库及表信息
-- ------------------seata-server数据库及表信息 start------------------------------------
CREATE DATABASE seata-server;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(0) NULL DEFAULT NULL,
`gmt_modified` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(0) NULL DEFAULT NULL,
`gmt_modified