原理分析,设计到的业务请参考系列的第二篇。
Seata之AT模式Nacos版实战(二)
下面两个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
}]]
}]]
}
}]]
}
{
"@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
}]]
}]]
}
}]]
}
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.
AT模式如何做到对业务的无侵入?
因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
-- 查看当前数据库的隔离级别,mysql默认为READ-COMMITTED
show variables like 'transaction_isolation';
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
如果遇到存在高并发并且对于数据的准确性很有要求的场景,需要使用for update。
比如涉及到金钱、库存等。一般这些操作都是很长一串并且是开启事务的。如果库存刚开始读的时候是1,而立马另一个进程进行了update将库存更新为0了,而事务还没有结束,会将错的数据一直执行下去,就会有问题。所以需要for upate 进行数据加锁防止高并发时候数据出错。
set autocommit = 0;
begin;
select * from tab_order where id = 1 for update;
-- 等第二个窗口执行完成之后再执行commit
commit;
update tab_order set product_id = 100 where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
再对id=2的数据进行update name操作,发现成功
update tab_order set product_id = 200 where id = 2;
Query OK, 1 row affected (0.00 sec)