Seata 全局锁用于保证不同分布式事务的写隔离和读隔离,即避免脏写、脏读问题。
对于读写隔离原理官方画的图还是比较清晰的: https://seata.apache.org/zh-cn/docs/dev/mode/at-mode。
-
脏写问题
由于AT模式基于快照补偿,如果一个全局事务写操作发生在另一个全局事务写操作和回滚操作之间,很可能导致脏写问题(更新丢失);
引入了全局锁后可以避免一个事务的写操作发生在另一个全局事务写操作和回滚操作之间。
无全局锁时导致脏写的场景:
还是以购物场景为例,包含创建订单、扣库存、扣存款余额,这里只举一个分支事务例子即可 ;假设两个全局事务GTX1 GTX2 执行时间线:
GTX1 全局事务开始 -> GTX2 全局事务开始 -> GTX1 扣库存并提交(库存100改为99) -> GTX2扣库存并提交(库存99改为98)-> GTX2 全局事务提交结束 -> GTX1 全局事务后续出现异常回滚(执行UndoLog补偿)-> 库存改回100。
实际库存应该是99才对。
全局锁解决上面场景脏写的原理:
每个全局事务执行完一个分支事务SQL后在分支事务本地提交前需要向 TC 注册分支事务同时请求获取全局锁,锁住数据表被修改的行,等到全局事务提交或回滚后才释放全局锁;这个过程中其他全局事务修改同一行在提交前同样需要先获取全局锁,会发现锁已经被占用会不断重试获取锁,获取锁成功后回滚本地事务并重新执行。
加全局锁后的时间线:
GTX1 全局事务开始 -> GTX2 全局事务开始 -> GTX1 扣库存并提交(库存100改为99,获取全局锁) -> GTX2扣库存(库存99改为98)但是无法获取全局锁每次间隔10ms不断重试获取锁 -> GTX1 全局事务后续出现异常回滚(执行UndoLog补偿)-> 库存改回100 并释放全局锁 -> GTX2 成功获取全局锁,回滚本地事务后重新执行。
-
脏读问题
由于分支事务在一阶段直接提交,这样可能导致其他全局事务能够看到当前全局事务已修改(分支事务提交)但是全局事务尚未提交的数据,这就属于脏读问题,这也是Seata官方为何说 Seata 默认全局隔离级别是读未提交。
Seata 内解决这个问题的方案是拦截 SELECT FOR UPDATE 语句,添加获取全局锁的逻辑。
所以解决脏读问题,用户还需要使用 SELECT FOR UPDATE 语句代替 SELECT 语句。