新项目需要用seata,于是研究了下seata是啥,官网来回答
官网有说明AT模式能满足绝大多数的使用场景,于是咱就研究下AT模式。AT模式是基于数据库本身的事务机制来做的全局事务。基于两阶段提交的形式:
第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
第二阶段:
这两阶段中就可能会有投机分子的可乘之机,看下面的图
啊哈,全局事务翻车了,紧接着发生了啥呢? seata就会报错并一直重试下去。 我滴个乖乖,真的是一直重试,貌似一直等到某一天id = 1这条记录突然又被set a = 1 为止。
2023-07-25 22:15:25.795 INFO 62854 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=172.17.0.2:8091:18431217438548037,branchId=18431217438548038,branchType=AT,resourceId=jdbc:mysql://47.94.201.120:13306/db_seata,applicationData=null
2023-07-25 22:15:25.795 INFO 62854 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.17.0.2:8091:18431217438548037 18431217438548038 jdbc:mysql://47.94.201.120:13306/db_seata
2023-07-25 22:15:25.825 INFO 62854 --- [tch_RMROLE_1_16] i.s.r.d.undo.AbstractUndoExecutor : Field not equals, name count, old value 991, new value 1991
2023-07-25 22:15:25.845 INFO 62854 --- [tch_RMROLE_1_16] i.seata.rm.datasource.DataSourceManager : branchRollback failed reason [Branch session rollback failed and try again later xid = 172.17.0.2:8091:18431217438548037 branchId = 18431217438548038 Has dirty records when undo.]
2023-07-25 22:15:25.845 INFO 62854 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_RollbackFailed_Retryable
2023-07-25 22:15:26.794 INFO 62854 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=172.17.0.2:8091:18431217438548037,branchId=18431217438548038,branchType=AT,resourceId=jdbc:mysql://47.94.201.120:13306/db_seata,applicationData=null
2023-07-25 22:15:26.794 INFO 62854 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.17.0.2:8091:18431217438548037 18431217438548038 jdbc:mysql://47.94.201.120:13306/db_seata
2023-07-25 22:15:26.826 INFO 62854 --- [tch_RMROLE_1_16] i.s.r.d.undo.AbstractUndoExecutor : Field not equals, name count, old value 991, new value 1991
2023-07-25 22:15:26.846 INFO 62854 --- [tch_RMROLE_1_16] i.seata.rm.datasource.DataSourceManager : branchRollback failed reason [Branch session rollback failed and try again later xid = 172.17.0.2:8091:18431217438548037 branchId = 18431217438548038 Has dirty records when undo.]
2023-07-25 22:15:26.847 INFO 62854 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_RollbackFailed_Retryable
seata版本
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
翻翻源码报错的位置
心想这应该算是个bug吧,seata估计也早就发现了,于是升级seata jar包版本到1.7.0之后
同样的现象复现后,seata改变了处理机制,不再进行重试,仅仅是报出一个SQLUndoDirtyException,然后回滚操作就结束了,并没有执行undo_log表解析出来的回滚sql.
只记录了一个log日志,提示需要手动处理相关表的相关行,并手动处理undo_log表中的记录。
2023-07-25 23:45:44.913 ERROR 69507 --- [h_RMROLE_1_3_16] i.seata.rm.datasource.DataSourceManager : branchRollback failed. branchType:[AT], xid:[172.17.0.2:8091:18431217438548045], branchId:[18431217438548046], resourceId:[jdbc:mysql://47.94.201.121:13306/db_seata], applicationData:[null]. reason:[Branch session rollback failed because of dirty undo log, please delete the relevant undolog after manually calibrating the data. xid = 172.17.0.2:8091:18431217438548045 branchId = 18431217438548046]
2023-07-25 23:45:44.913 INFO 69507 --- [h_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_RollbackFailed_Unretryable
这个现象被称为脏写,如何解决呢?
对于dirty undo log的异常日志,要有捕获机制并及时告警。
冲突已发生过了,脏数据也产生了,这时候只能人工分析日志和库表数据来进行手动补偿了。
官网有提供一个注解@GlobalLock,像上面提到的产生脏写的事务B方法上如果加上了@GlobalLock注解,则事务B在提交前会去检查自己update的记录有没有全局锁存在,如果有全局锁的话就会等待全局锁消失后再提交,@GlobalLock自己也提供了重试间隔和重试次数 (但是只有配置for update才会生效)。
但是!这里会有个问题,事务B在提交前已经占有了记录id=1的行锁,这里它在等待全局锁释放前不会释放行锁。如果此时事务A发现有全局事务中有个别分支事务失败了,则需要回滚,回滚sql执行时也需要获取id = 1的行锁,这不就卡住了!全局事务A的回滚过程只能等待事务B超时后(超时后释放事务拥有的锁)才能正确执行回滚sql。
SO,怎么解决锁冲突呢? 使用@GlobalLock+select for update 组合
事务B方法上加了@GlobalLock后,对可能与全局锁冲突的行先执行 select for update操作去提前获取锁,由于for update操作会与全局锁做锁等待,所以事务B在获取到for update指定的行锁前就不再执行下面的update语句,即没有拥有行锁,全局事务A如果需要回滚的话也不会发生锁冲突。
补充一点,我的seata服务端是用docker启动的1.7.0版本的,项目用的网上的demo所以一开始没注意客户端的版本号,恰好也就有了上面的发现。
总结:生产中对于非全局事务可能与全局事务发生锁冲突的情况,使用@GlobalLock+select for update 组合来进行锁等待。如果怕代码中没关注到这个冲突情况,记得做好日志监控,手动处理数据。