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

新项目需要用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>

翻翻源码报错的位置

 undo_log执行发现脏写情况后报了一个SQLException,上层方法undo中捕获到异常后又抛出了一个BranchRollbackFailed_Retriable的异常,提示回滚操作进行重试,怪不得会一直重试呢,简直死循环了。

心想这应该算是个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 组合来进行锁等待。如果怕代码中没关注到这个冲突情况,记得做好日志监控,手动处理数据。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Seata 是一个开源的分布式事务解决方案,支持多种语言和框架。在使用 Seata 进行全局事务时,需要在代码中添加 Seata 的相关配置和 API 调用。下面是一个简单的示例: 1. 配置 Seata: 在项目的配置文件中,添加 Seata 的配置信息,包括注册中心地址、事务组名称等。例如,在 Spring Boot 中,可以在 application.properties 文件中添加如下配置: ``` spring.application.name=your-application-name spring.cloud.alibaba.seata.tx-service-group=your-tx-service-group-name spring.cloud.alibaba.seata.registry-type=service spring.cloud.alibaba.seata.service-registry-address=your-registry-address spring.cloud.alibaba.seata.config.enabled=true ``` 2. 编事务代码: 在需要进行全局事务的方法上,加上 @GlobalTransactional 注解,即可开启全局事务。例如: ``` @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private OrderService orderService; @GlobalTransactional public void createUserAndOrder(User user, Order order) { userMapper.createUser(user); orderService.createOrder(order); } } ``` 在上面的示例中,当 createUserAndOrder 方法被调用时,Seata 会自动开启一个全局事务,并在其中包含 userMapper.createUser 和 orderService.createOrder 两个方法的本地事务。如果其中任何一个方法抛出异常,Seata 会自动回滚所有本地事务全局事务。 需要注意的是,Seata 只能管理支持 XA 协议的数据源,如果使用的是非 XA 数据源,需要进行额外的配置和开发工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值