Seata的AT模式会引发脏读吗?如何解决?

后端 潘老师 2周前 (04-09) 25 ℃ (0) 扫码查看

Seata是一款分布式事务处理应用的框架,不少开发者心中都有一个疑问:Seata的AT模式会不会出现脏读呢?答案是肯定的,不过它出现的脏读情况和传统意义上的脏读有所不同。传统脏读指的是在MySQL本地事务场景下,一个事务读取到了其他未提交事务的数据。而Seata的AT模式中,存在这样一种情况:一个事务可能读取到其他分支事务(也是本地事务)已提交,但后续可能因全局事务回滚而撤销的数据。这听起来有些绕,下面详细讲解,看完后大家就能理解了。

要想深入理解这个问题,首先得清楚Seata的AT模式的工作原理。AT模式的核心机制是两阶段提交:

  • 第一阶段:本地事务会立即提交,同时释放本地锁,这使得数据对其他事务可见。
  • 第二阶段:全局事务会依据协调结果来决定最终是提交还是回滚,这个过程借助undo log来进行补偿操作。

了解了工作原理后,我们来设想一个场景。假设有三个模块,分别是交易模块、订单模块和库存模块。在一次下单过程中,为确保数据的一致性,代码可能会像下面这样编写:

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;

@Service
public class TradeService {
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private OrderService orderService;

    @GlobalTransactional
    public boolean buy() {
        //库存扣减   
        inventoryService.decreaseInvenroty();       
        //创建订单   
        orderService.createOrder();  
    }  
}

在这段代码里,@GlobalTransactional开启了一个分布式事务。在这个事务中,会先调用库存服务进行库存扣减,然后再调用订单服务创建订单。整个大致的流程是这样的:

  1. 在交易模块(Trade)中,首先创建全局事务。
  2. 库存模块(Inventory)介入:
    • 2.1注册分支事务。
    • 2.2进行库存扣减操作,并记录undo log 。这里要注意,库存模块在数据库上的操作是基于数据库的本地事务进行的。之所以借助本地事务,是为了保障undo log(Seata使用的undo log和MySQL中MVCC的undo log不是同一个概念)和库存扣减操作的原子性。而且,这一步执行完成后,数据库的本地事务会提交。
  3. 订单模块(Order)参与:
    • 3.1注册分支事务。
    • 3.2创建订单,并记录undo log 。

这里关键的一点是,当库存模块完成扣减操作,数据库的本地事务提交后,不管处于何种事务隔离级别,其他事务都能查询到提交后的新值。想象一下,如果库存扣减成功,但在创建订单时失败了,此时整个分布式事务需要回滚,会依据库存库中的undo log进行回滚操作。那么在库存模块提交后、全局事务回滚前,如果有其他事务来查询库存数据,就会读到一个本该回滚的值。这就是Seata的AT模式下出现的脏读情况,它发生在全局事务的过程中,其他事务读到了全局事务尚未最终确定提交(后续可能会回滚)的数据。

既然AT模式存在这样的脏读问题,那有没有办法避免呢?确实有一个办法,就是在查询时使用@GlobalTransactional + select * ... for update 。不过这个方法比较笨拙,它虽然能解决脏读问题,但在实际应用中会带来一些弊端。因为事务已经提交,要避免其他事务读取数据比较困难,即便实现了,也会极大地降低系统的可用性。所以,如果项目中对脏读的容忍度为零,不接受这种情况,那就不建议使用AT模式,可以考虑选择其他事务方案,例如TCC模式。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/17024.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】