分布式事务(六):Seata之AT模式原理

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需要重试等待 全局锁。

 0

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

3.2.2、事务回滚原理

  若tx1的二阶段全局回滚,则tx1需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果tx2仍在等待该数据的 全局锁,同时持有本地锁,则tx1的分支回滚会失败。tx1分支的回滚会一直重试,直到tx2的全局锁 等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。

 

0

  整个过程全局锁在tx1结束前一直是被tx1持有,所以不会发生脏写问题。

4、读隔离

  数据库本地事务隔离级别 读已提交(Read Committed) 或 以上的基础上,Seata (AT模式)的默认全局隔离级别是 读未提交(Read Uncommitted)。

  若应用在特定场景下,必须要求全局的读已提交,Seata2.0的方式是通过 SELECT FOR UPDATE 语句的代理。

 0

  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;

 

posted @ 2024-02-07 17:07  无虑的小猪  阅读(170)  评论(0编辑  收藏  举报