基于本文回答
0
评论

Seata AT 模式下是如何解决全局事务并发导致的脏写(Dirty Write)问题的?

知识点图片

在 Seata AT 模式下,解决全局事务并发导致的脏写(Dirty Write)问题的核心机制是:全局锁(Global Lock) + 本地数据校验(Undo Log Image 对比)

要理解这个机制,首先需要明白为什么在 AT 模式下会产生脏写风险。

一、 为什么 AT 模式会有脏写风险?

Seata AT 模式是一个改进版的两阶段提交(2PC):

  • 一阶段(Phase 1): 执行业务 SQL,生成 Undo Log,然后直接提交本地数据库事务,释放本地数据库的行锁。
  • 二阶段(Phase 2): 如果全局事务提交,则异步清理 Undo Log;如果全局事务回滚,则通过 Undo Log 反向补偿数据。

风险点在于: 一阶段提交后,本地数据库锁已经释放。此时,如果另一个事务(全局或本地)来修改同一行数据,并在当前全局事务的二阶段回滚之前提交了修改。当当前全局事务需要回滚时,就会把别人的修改覆盖掉,从而产生脏写


二、 Seata 是如何解决脏写的?

Seata 主要通过以下三层防线来彻底解决或拦截脏写问题:

第一层防线:全局锁(Global Lock)—— 针对“全局事务 vs 全局事务”

这是 Seata 防止脏写最核心的机制。全局锁由 Seata 的 TC(Transaction Coordinator,事务协调器)统一维护。

  1. 一阶段:获取全局锁
    • 当 RM(资源管理器)执行完本地 SQL,准备提交本地数据库事务之前,它必须先向 TC 申请该行数据的全局锁(锁的标识通常是:表名 + 主键)。
    • 如果申请成功: 提交本地事务,释放本地数据库行锁。
    • 如果申请失败: 说明有其他全局事务正在操作这行数据,当前事务会不断重试,直到超时(抛出异常,触发全局回滚)。
  2. 二阶段:释放全局锁
    • 全局提交(Commit): TC 收到全局提交指令,释放该行数据的全局锁,并异步通知 RM 清理 Undo Log。
    • 全局回滚(Rollback): TC 通知 RM 进行数据补偿(执行 Undo Log),补偿完成后,TC 再释放全局锁。

工作示例:
假设全局事务 A 和 全局事务 B 并发修改同一行记录(ID=1):

  • 事务 A 先拿到本地锁,执行 SQL,向 TC 申请到 ID=1全局锁,然后提交本地事务,释放本地锁。
  • 事务 B 此时可以拿到本地锁,执行 SQL。但在提交本地事务前,B 必须向 TC 申请 ID=1 的全局锁。
  • 因为全局锁在 A 手里(A 的二阶段还没结束),B 申请全局锁失败,B 只能等待
  • 这样就严格防止了 B 覆盖 A 的数据,避免了并发全局事务下的脏写。

第二层防线:Undo Log 数据校验 —— 针对“全局事务 vs 纯本地事务”

如果系统中存在绕过 Seata TC 的纯本地事务(即普通的 Spring @Transactional,不带 @GlobalTransactional),纯本地事务是不会去 TC 申请全局锁的,它直接修改数据库并提交。

这种情况下,全局锁防不住纯本地事务,脏写可能发生。Seata 通过二阶段回滚时的校验机制来兜底:

  1. 在一阶段,Seata 会生成数据的 before_image(修改前的数据)和 after_image(修改后的数据),保存在 undo_log 表中。
  2. 在二阶段如果发生回滚,Seata 不会盲目地用 before_image 恢复数据,而是会先进行数据校验
  3. 校验规则:对比当前数据库中的实际数据 与 after_image 是否一致。
    • 如果一致: 说明这期间没有其他本地事务修改过这行数据,允许安全回滚(用 before_image 覆盖回去)。
    • 如果不一致: 说明发生了脏写(被纯本地事务偷偷改了)。此时 Seata 拒绝回滚,因为如果回滚就会把本地事务的修改覆盖掉。Seata 会抛出异常,并转为人工介入处理(通常需要开发者通过告警发现,并手动修复数据)。

第三层防线:@GlobalLock 注解 —— 优雅处理本地事务

为了解决第二层防线中“纯本地事务修改导致全局事务无法回滚,只能人工介入”的痛点,Seata 提供了 @GlobalLock 注解。

如果你的系统中有一部分业务是只需要操作单库的本地事务,但它操作的表又会参与到其他的全局事务中,你应该在这个本地事务的方法上加上 @GlobalLock 注解。

  • 作用: 加了 @GlobalLock 的本地事务,虽然不会开启全局事务,但在提交本地事务前,它会去 TC 检查该行记录是否被其他全局事务加了全局锁
  • 效果: 如果有全局锁,本地事务会等待/重试,从而避免了它去破坏正在进行中的全局事务的数据,从根源上消除了“全局事务 vs 本地事务”导致的脏写问题。

总结

Seata AT 模式解决脏写(并发写)问题的核心逻辑如下:

并发场景 防御机制 结果
全局事务 vs 全局事务 TC 全局锁互斥排队 后一个事务等待前一个事务的二阶段结束,完全避免脏写
全局事务 vs 本地事务(带 @GlobalLock) TC 全局锁互斥排队 本地事务等待全局事务的二阶段结束,完全避免脏写
全局事务 vs 普通本地事务 Undo Log after_image 校验 拦截非法回滚,拒绝覆盖别人的修改,抛出异常交由人工处理

获取锁的顺序(非常关键以避免死锁):
一阶段执行时:先获取本地数据库锁 -> 执行业务 SQL -> 获取 Seata 全局锁 -> 释放本地数据库锁。

右滑查看面试常问