Seata的AT模式会引发脏读吗?如何解决?
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
开启了一个分布式事务。在这个事务中,会先调用库存服务进行库存扣减,然后再调用订单服务创建订单。整个大致的流程是这样的:
- 在交易模块(Trade)中,首先创建全局事务。
- 库存模块(Inventory)介入:
- 2.1注册分支事务。
- 2.2进行库存扣减操作,并记录undo log 。这里要注意,库存模块在数据库上的操作是基于数据库的本地事务进行的。之所以借助本地事务,是为了保障undo log(Seata使用的undo log和MySQL中MVCC的undo log不是同一个概念)和库存扣减操作的原子性。而且,这一步执行完成后,数据库的本地事务会提交。
- 订单模块(Order)参与:
- 3.1注册分支事务。
- 3.2创建订单,并记录undo log 。
这里关键的一点是,当库存模块完成扣减操作,数据库的本地事务提交后,不管处于何种事务隔离级别,其他事务都能查询到提交后的新值。想象一下,如果库存扣减成功,但在创建订单时失败了,此时整个分布式事务需要回滚,会依据库存库中的undo log进行回滚操作。那么在库存模块提交后、全局事务回滚前,如果有其他事务来查询库存数据,就会读到一个本该回滚的值。这就是Seata的AT模式下出现的脏读情况,它发生在全局事务的过程中,其他事务读到了全局事务尚未最终确定提交(后续可能会回滚)的数据。
既然AT模式存在这样的脏读问题,那有没有办法避免呢?确实有一个办法,就是在查询时使用@GlobalTransactional + select * ... for update
。不过这个方法比较笨拙,它虽然能解决脏读问题,但在实际应用中会带来一些弊端。因为事务已经提交,要避免其他事务读取数据比较困难,即便实现了,也会极大地降低系统的可用性。所以,如果项目中对脏读的容忍度为零,不接受这种情况,那就不建议使用AT模式,可以考虑选择其他事务方案,例如TCC模式。