1、整体机制
Seata AT模型,基于本地ACID事务的关系型数据库实现。
两阶段提交协议机制如下:
一阶段:业务数据和回滚日志在同一个本地事务中提交,释放本地锁和连接资源
二阶段:提交异步化,非常快速地完成;回滚通过一阶段的回滚日志进行反向补偿
2、事务状态
global_table是seata的全局事务表,可以通过global_table表中status字段知悉全局事务处于哪个状态。
2.1、全局事务状态表
状态 | 码值 | 备注 |
全局事务开始(Begin) | 1 | 此状态可以接受新的分支事务注册 |
全局事务提交中(Committing) | 2 | 这个状态会随时改变 |
全局事务提交重试(CommitRetry) | 3 | 在提交异常被解决后尝试重试提交 |
全局事务回滚中(Rollbacking) | 4 | 正在重新回滚全局事务 |
全局事务回滚重试中(RollbackRetrying) | 5 | 在全局回滚异常被解决后尝试事务重试回滚中 |
全局事务超时回滚中(TimeoutRollbacking) | 6 | 全局事务超时回滚中 |
全局事务超时回滚重试中(TimeoutRollbackRetrying) | 7 | 全局事务超时回滚重试中 |
异步提交中(AsyncCommitting) | 8 | 异步提交中 |
二阶段已提交(Committed) | 9 | 二阶段已提交,此状态后全局事务状态不会再改变 |
二阶段提交失败(CommitFailed) | 10 | 二阶段提交失败 |
二阶段决议全局回滚(Rollbacked) | 11 | 二阶段决议全局回滚 |
二阶段全局回滚失败(RollbackFailed) | 12 | 二阶段全局回滚失败 |
二阶段超时回滚(TimeoutRollbacked) | 13 | 二阶段超时回滚 |
二阶段超时回滚失败(TimeoutRollbackFailed) | 14 | 二阶段超时回滚失败 |
全局事务结束(Finished) | 15 | 全局事务结束 |
二阶段提交超时(CommitRetryTimeout) | 16 | 二阶段提交因超过重试时间限制导致失败 |
二阶段回滚超时(RollbackRetryTimeout) | 17 | 二阶段回滚因超过重试时间限制导致失败 |
未知状态(UnKnown) | 0 | 未知状态 |
2.2、分支事务状态表
状态 | 码值 | 备注 |
分支事务注册(Registered) | 1 | 向TC注册分支事务 |
分支事务一阶段完成(PhaseOne_Done) | 2 | 分支事务一阶段业务逻辑完成 |
分支事务一阶段失败(PhaseOne_Failed) | 3 | 分支事务一阶段业务逻辑失败 |
分支事务一阶段超时(PhaseOne_Timeout) | 4 | 分支事务一阶段处理超时 |
分支事务二阶段已提交(PhaseTwo_Committed) | 5 | 分支事务二阶段提交 |
分支事务二阶段提交失败重试(PhaseTwo_CommitFailed_Retryable) | 6 | 分支事务二阶段提交失败重试 |
分支事务二阶段提交失败不重试(PhaseTwo_CommitFailed_Unretryable) | 7 | 分支事务二阶段提交失败不重试 |
分支事务二阶段已回滚(PhaseTwo_Rollbacked) | 8 | 分支事务二阶段已回滚 |
分支事务二阶段回滚失败重试(PhaseTwo_RollbackFailed_Retryable) | 9 | 分支事务二阶段回滚失败重试 |
分支事务二阶段回滚失败不重试(PhaseTwo_RollbackFailed_Unretryable) | 10 | 二阶段提交失败 |
未知状态(UnKnown) | 0 | 未知状态 |
2.3、全局事务超时回滚中(TimeoutRollbacking)
当某个seata全局事务执行过程中,无法完成业务。
TC中的一个定时任务(专门用来寻找已超时的全局事务),发现该全局事务未回滚完成,就会将此全局事务改为全局事务超时回滚中(TimeoutRollbacking),开始回滚,直到回滚完毕后删除global_table数据。
当全局事务处于该状态,请排查为何业务无法在限定时间内完成事务。若确实无法完成,应调大全局事务超时时间。(如排查一切正常,请检查tc集群时区与数据库是否一致,若不一致请改为一致)。
3、写隔离
3.1、写隔离原理
一阶段本地事务提交前,需要确保先拿到全局锁;
拿不到全局锁,不能提交本地事务;
拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
3.2、提交回滚原理
两个全局事务tx1和tx2,分别对 a 表的 m 字段进行更新操作,m的初始值1000。
3.2.1、事务提交原理
tx1先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前先拿到该记录的全局所,本地提交释放本地锁。
tx2后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁,tx1全局提交前,该记录的全局锁被tx1持有,tx2需要重试等待 全局锁。
tx1二阶段全局提交,释放全局锁;tx2 拿到全局锁 提交本地事务。
3.2.2、事务回滚原理
若tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果tx2仍在等待该数据的 全局锁,同时持有本地锁,则tx1的分支回滚会失败。tx1分支的回滚会一直重试,直到tx2的全局锁 等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
整个过程全局锁在tx1结束前一直是被tx1持有,所以不会发生脏写问题。
4、读隔离
数据库本地事务隔离级别 读已提交(Read Committed) 或 以上的基础上,Seata (AT模式)的默认全局隔离级别是 读未提交(Read Uncommitted)。
若应用在特定场景下,必须要求全局的读已提交,Seata2.0的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁,若全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE语句的本地执行)并重试。这个过程中,查询会被block住的,直到全局锁拿到,即读取的相关数据是已提交的才返回。
Seata2.0目前的并没有对所有的SELECT语句进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
5、工作机制
用如下示例表示AT分支的工作过程:
业务表 product:
Field | Type | Key |
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
5.1、一阶段
1、解析SQL:得到SQL的类型(UPDATE)、表(product)、条件(where name = 'TXC')等相关的信息;
2、查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据;
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
1 | TXC | 2014 |
3、执行业务SQL:更新这条记录的name为'GTS';
4、查询后镜像:根据前镜像的结果,通过 主键 定位数据;
select id, name, since from product where id = 1;
得到后镜像:
id | name | since |
1 | GTS | 2014 |
5、插入回滚日志:把前后镜像数据以及业务SQL相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中;
{ "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" }, "sqlType": "UPDATE" }], "xid": "xid:xxx" }
6、提交前,向 TC 注册分支:申请 product 表中,主键值等于1的记录的全局锁;
7、本地事务提交:业务数据的更新和前面步骤中生成的UNDO LOG一并提交;
8、将本地事务提交的结果上报给TC。
5.2、二阶段
5.2.1、回滚
1、收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作;
2、通过 XID 和 Branch ID查找到相应的 UNDO LOG 记录;
3、数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如有不同,说明数据被当前全局事务之外的动作做了修改。
4、根据 UNDO LOG 中的前镜像和业务SQL的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
5、提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。
5.2.2、提交
1、收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC;
2、异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
5.3、回滚日志表
CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
标签:回滚,Seata,事务,name,阶段,提交,全局,分布式 From: https://www.cnblogs.com/RunningSnails/p/18011084