• Seata之AT模式原理详解(三)


    概述

    原理分析,设计到的业务请参考系列的第二篇。
    Seata之AT模式Nacos版实战(二)

    TC相关的表解析

    • global_table:全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID。
    • branch_table:分支事务表,记录每一个分支事务的ID,分支事务操作的哪个数据库等信息。
    • lock_table:全局锁表。

    日志分析

    undo_log日志分析

    下面两个json分别是两个业务服务的undo_log日志。

    {
    	"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
    	"xid": "192.168.2.196:8091:104983180048351232",
    	"branchId": 104983207323910145,
    	"sqlUndoLogs": ["java.util.ArrayList", [{
    		"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
    		"sqlType": "UPDATE",
    		"tableName": "tab_storage",
    		"beforeImage": {
    			"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
    			"tableName": "tab_storage",
    			"rows": ["java.util.ArrayList", [{
    				"@class": "io.seata.rm.datasource.sql.struct.Row",
    				"fields": ["java.util.ArrayList", [{
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "id",
    					"keyType": "PRIMARY_KEY",
    					"type": -5,
    					"value": ["java.lang.Long", 1]
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "total",
    					"keyType": "NULL",
    					"type": 4,
    					"value": 88
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "used",
    					"keyType": "NULL",
    					"type": 4,
    					"value": 12
    				}]]
    			}]]
    		},
    		"afterImage": {
    			"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
    			"tableName": "tab_storage",
    			"rows": ["java.util.ArrayList", [{
    				"@class": "io.seata.rm.datasource.sql.struct.Row",
    				"fields": ["java.util.ArrayList", [{
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "id",
    					"keyType": "PRIMARY_KEY",
    					"type": -5,
    					"value": ["java.lang.Long", 1]
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "total",
    					"keyType": "NULL",
    					"type": 4,
    					"value": 87
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "used",
    					"keyType": "NULL",
    					"type": 4,
    					"value": 13
    				}]]
    			}]]
    		}
    	}]]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    {
    	"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
    	"xid": "192.168.2.196:8091:104983180048351232",
    	"branchId": 104983197731536896,
    	"sqlUndoLogs": ["java.util.ArrayList", [{
    		"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
    		"sqlType": "INSERT",
    		"tableName": "tab_order",
    		"beforeImage": {
    			"@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords",
    			"tableName": "tab_order",
    			"rows": ["java.util.ArrayList", []]
    		},
    		"afterImage": {
    			"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
    			"tableName": "tab_order",
    			"rows": ["java.util.ArrayList", [{
    				"@class": "io.seata.rm.datasource.sql.struct.Row",
    				"fields": ["java.util.ArrayList", [{
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "id",
    					"keyType": "PRIMARY_KEY",
    					"type": -5,
    					"value": ["java.lang.Long", 18]
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "user_id",
    					"keyType": "NULL",
    					"type": -5,
    					"value": ["java.lang.Long", 1]
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "product_id",
    					"keyType": "NULL",
    					"type": -5,
    					"value": ["java.lang.Long", 1]
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "count",
    					"keyType": "NULL",
    					"type": 4,
    					"value": null
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "money",
    					"keyType": "NULL",
    					"type": 3,
    					"value": ["java.math.BigDecimal", 88]
    				}, {
    					"@class": "io.seata.rm.datasource.sql.struct.Field",
    					"name": "status",
    					"keyType": "NULL",
    					"type": 4,
    					"value": null
    				}]]
    			}]]
    		}
    	}]]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    系统日志

    2021-02-16 16:45:40.728  INFO --- [ServerHandlerThread_1_4_500] i.s.s.coordinator.DefaultCoordinator     : Begin new global transaction applicationId: business-service,transactionServiceGroup: business-service-tx-group, transactionName: buy(long, long),timeout:60000,xid:192.168.2.196:8091:104983180048351232
    2021-02-16 16:45:44.714  INFO --- [batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler         : xid=192.168.2.196:8091:104983180048351232,branchType=AT,resourceId=jdbc:mysql://rm-bp17dq6iz79761b8fxo.mysql.rds.aliyuncs.com:3306/it235_order,lockKey=tab_order:18,clientIp:192.168.2.196,vgroup:order-service-tx-group
    2021-02-16 16:45:44.935  INFO --- [ServerHandlerThread_1_5_500] i.seata.server.coordinator.AbstractCore  : Register branch successfully, xid = 192.168.2.196:8091:104983180048351232, branchId = 104983197731536896, resourceId = jdbc:mysql://rm-bp17dq6iz79761b8fxo.mysql.rds.aliyuncs.com:3306/it235_order ,lockKeys = tab_order:18
    2021-02-16 16:46:40.917  INFO --- [TxTimeoutCheck_1_1] i.s.s.coordinator.DefaultCoordinator     : Global transaction[192.168.2.196:8091:104983180048351232] is timeout and will be rollback.
    2021-02-16 16:46:42.280  INFO --- [RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore  : Rollback branch transaction successfully, xid = 192.168.2.196:8091:104983180048351232 branchId = 104983207323910145
    2021-02-16 16:46:42.658  INFO --- [RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore  : Rollback branch transaction successfully, xid = 192.168.2.196:8091:104983180048351232 branchId = 104983197731536896
    2021-02-16 16:46:42.720  INFO --- [RetryRollbacking_1_1] io.seata.server.coordinator.DefaultCore  : Rollback global transaction successfully, xid = 192.168.2.196:8091:104983180048351232.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    AT优势

    AT模式如何做到对业务的无侵入?

    一阶段步骤

    • TM:bussiness-service.buy(long,long)方法执行时,由于该方法具有@GlobalTransactional标志,该TM会向TC发起全局事务,生成XID(全局锁)。
    • RM:OrderService.create(long,long):写表,undo_log记录回滚日志(Branch ID),通知TC操作结果。
    • RM:StorageService.changeNum(long,long):写表,undo_log记录回滚日志(Branch ID),通知TC操作结果。
      RM写表的过程,Seata会拦截业务SQL,首先解析SQL语义,在业务数据被更新前,将其保存成before image,然后执行业务SQL,在业务数据更新之后,再将其保存成after image,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
      在这里插入图片描述

    二阶段步骤

    因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

    • 正常:TM执行成功,通知TC全局提交,TC此时通知所有的RM提交成功,删除UNDO_LOG回滚日志。
      在这里插入图片描述
    • 异常
      TM执行失败,通知TC全局回滚,TC此时通知所有的RM进行回滚,根据UNDO_LOG反向操作,使用before image还原业务数据,删除UNDO_LOG,但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
      在这里插入图片描述
      AT模式的一阶段、二阶段提交和回滚均由Seata框架自动生成,用户只需编写业务SQL,便能轻松接入分布式事务,AT模式是一种对业务无任何侵入的分布式事务解决方案。

    读写隔离

    写隔离

    • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
    • 拿不到 全局锁 ,不能提交本地事务。
    • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
      以一个示例来说明:
      两个全局事务 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) 。

    -- 查看当前数据库的隔离级别,mysql默认为READ-COMMITTED
    show variables like 'transaction_isolation';
    
    • 1
    • 2

    如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
    在这里插入图片描述
    SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
    出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

    for update扩展

    使用场景

    如果遇到存在高并发并且对于数据的准确性很有要求的场景,需要使用for update。
    比如涉及到金钱、库存等。一般这些操作都是很长一串并且是开启事务的。如果库存刚开始读的时候是1,而立马另一个进程进行了update将库存更新为0了,而事务还没有结束,会将错的数据一直执行下去,就会有问题。所以需要for upate 进行数据加锁防止高并发时候数据出错。

    • for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。
    • 要测试for update的锁表情况,可以利用MySQL的Command Mode,开启二个视窗来做测试。

    窗口模拟

    • 窗口A,非自动提交事务,用于for update操作;
    set autocommit = 0;
    begin;
    select * from tab_order where id = 1 for update;
    
    -- 等第二个窗口执行完成之后再执行commit
    commit;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 窗口B,用于普通update操作。
      在b窗口对id=1的数据进行update name操作,发现失败:等待锁释放超时
    update tab_order set product_id = 100 where id = 1;
    ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
    
    • 1
    • 2

    再对id=2的数据进行update name操作,发现成功

    update tab_order set product_id = 200 where id = 2;
    Query OK, 1 row affected (0.00 sec)
    
    • 1
    • 2

    总结

    • for update操作在未获取到数据的时候,mysql不进行锁 (no lock)
    • 获取到数据的时候,进行对约束字段进行判断,存在有索引的字段则进行row lock 否则进行 table lock
    • 当使用 ‘<>’,'like’等关键字时,进行for update操作时,mysql进行的是table lock
  • 相关阅读:
    《MATLAB 神经网络43个案例分析》:第41章 定制神经网络的实现——神经网络的个性化建模与仿真
    古代汉语(王力版)笔记 绪论
    C 语言典范编程
    C#运算符重载
    Privacy Policy
    自学Python06-学会Python中的while循环语句
    Vue07/Vue自定义指令 局部注册 全局注册 自定义指令值传值
    xss 漏洞
    【Linux】5.0基础IO
    数据结构手写算法整理(考研)
  • 原文地址:https://blog.csdn.net/tianzhonghaoqing/article/details/125901267