您的当前位置:首页正文

seata全局事务回滚前,出现的脏写现象

2024-11-28 来源:个人技术集锦

新项目需要用seata,于是研究了下seata是啥,官网来回答

 官网有说明AT模式能满足绝大多数的使用场景,于是咱就研究下AT模式。AT模式是基于数据库本身的事务机制来做的全局事务。基于两阶段提交的形式:

第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

第二阶段:

  • 如果没什么异常,基本不用干啥大事,执行很快。
  • 如果某个分支事务发生了异常,需要全局回滚,这时候需要利用一阶段记录的回滚日志生成回滚sql,即再执行一个sql将数据还原回去。

脏写的现象

这两阶段中就可能会有投机分子的可乘之机,看下面的图

啊哈,全局事务翻车了,紧接着发生了啥呢? 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 组合来进行锁等待。如果怕代码中没关注到这个冲突情况,记得做好日志监控,手动处理数据。

显示全文