当前位置: 首页 > news >正文

Seata AT 模式的二阶段提交与“脏写”问题

1. 引言:ACID 的崩塌与 BASE 的崛起

在微服务架构下,随着业务的拆分,数据库也随之分库分表。传统的单机事务(ACID)在跨库、跨服务场景下失效。为了保证数据的一致性,我们引入了分布式事务。

常见的分布式事务解决方案包括:

  • XA (2PC): 强一致性,但阻塞严重,性能差。

  • TCC: 性能好,但业务侵入性极强,需要写 Try-Confirm-Cancel 三段逻辑。

  • Saga: 长事务解决方案,基于状态机,适合复杂业务流程。

  • Seata AT: 基于 2PC 的改进版,无侵入(自动生成 SQL),性能与一致性的折中方案。


2. Seata AT 模式原理:改进版的 2PC

Seata AT 模式的核心理念是:将两阶段提交的机制下沉到数据库层面的代理中,对业务代码无侵入。

2.1 两个阶段概览

  1. 一阶段(Phase 1):

    • 解析业务 SQL。

    • 查询操作前的数据(Before Image)。

    • 执行业务 SQL。

    • 查询操作后的数据(After Image)。

    • 生成 undo_log 记录。

    • 向 TC(Transaction Coordinator)注册分支,并申请全局锁。

    • 本地事务提交(注意:这里本地锁已经释放了!)。

  2. 二阶段(Phase 2):

    • 如果决议是 Commit:异步删除 undo_log,释放全局锁(速度极快)。

    • 如果决议是 Rollback:利用 undo_log 中的 Before Image 生成反向 SQL 进行补偿,释放全局锁。

3. 核心机制深度解析:UndoLog 与 全局锁

3.1 UndoLog 表结构

在使用 Seata AT 模式前,必须在每个业务库中创建 undo_log 表:

CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, -- 存储 Before/After Image `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

3.2 为什么需要全局锁(Global Lock)?

这是 Seata AT 区别于传统 XA 的关键。

  • XA:在 Phase 1 完成后,持有数据库本地锁,直到 Phase 2 结束才释放。这导致并发度极低。

  • Seata AT:在 Phase 1 结束时,直接提交本地事务,释放数据库本地锁

风险来了:本地锁释放了,意味着其他线程(甚至是不走 Seata 的本地事务)可以修改这条数据。如果此时全局事务需要回滚,数据已经被别人改脏了,怎么回滚?

这就需要全局锁。全局锁是由TC (Seata Server)维护的一张逻辑锁表。


4. 脏写问题与读写隔离分析 (Highlight)

我们来分析一个极端并发场景:写隔离(Write Isolation)

场景:Seata 全局事务 vs 此时的另一个事务

假设我们有一个表 account,字段 money = 100。

4.1 情况一:两个 Seata 全局事务并发写
  • Tx1开始,Update money = 90。

    • Tx1 获取本地锁。

    • Tx1 获取全局锁(Key: account:1)。

    • Tx1 提交本地事务,释放本地锁,但持有全局锁

  • Tx2开始,想要 Update money = 80。

    • Tx2 获取本地锁。

    • Tx2 尝试获取全局锁(Key: account:1)。

    • 结果:Tx2 获取全局锁失败(被 Tx1 占有)。Tx2 会进行重试(默认重试 30次,间隔 10ms)。

    • 如果 Tx1 在 Phase 2 提交或回滚后释放全局锁,Tx2 才能继续。

    • 如果 Tx2 等待超时,回滚本地事务。

结论Seata 所有的全局事务,依靠 TC 端的全局锁保证了互斥,不会出现脏写。

4.2 情况二:Seata 全局事务 vs 原生本地事务(最危险的情况)

如果不通过 Seata,直接用 JDBC 或者 Navicat 修改数据,会发生什么?

  • Tx1 (Seata):

    1. Update money = 100 -> 90。

    2. Before Image = 100, After Image = 90。

    3. 拿到全局锁,本地 Commit。

  • Tx2 (Native JDBC):

    1. Update money = 90 -> 80 (Tx2 不需要全局锁,直接拿到 DB 锁修改)。

    2. Tx2 Commit。

  • Tx1 发生异常,触发二阶段回滚:

    1. Tx1 对比当前数据库值(Current Value)与 Phase 1 的 After Image。

    2. Tx1 发现:Current(80) != After Image(90)。

    3. 结果回滚失败!这种情况被称为脏写导致的无法回滚。Seata 会记录异常日志,需要人工介入。

4.3 如何防止情况二?(@GlobalLock 的作用)

Seata 的设计前提是:为了数据一致性,所有涉及该表的写操作,都必须受 Seata 控制。

如果你有一个接口只需要本地事务,不需要分布式事务传播,但它涉及的表可能被其他全局事务操作,你应该使用 @GlobalLock 注解。

@Service public class AccountService { // 这是一个参与分布式事务的方法 @GlobalTransactional public void deduct(String userId, int money) { jdbcTemplate.update("update account set money = money - ? where id = ?", money, userId); } // 这是一个只做本地更新的方法,但为了防止破坏全局事务的一致性 // 加上 @GlobalLock,它会在执行 Update 前尝试检查全局锁 @GlobalLock @Transactional public void updateSafe(String userId, int money) { // Seata 代理会拦截: // 1. 获取本地锁 // 2. 尝试获取全局锁(如果拿不到,说明有全局事务正在操作这行数据,需等待) // 3. 执行 SQL // 4. 提交 jdbcTemplate.update("update account set money = ? where id = ?", money, userId); } }

4.4 读隔离(Read Isolation)

在 AT 模式下,默认的隔离级别是读未提交(Read Uncommitted)(针对全局视角)。
因为 Tx1 在 Phase 1 就提交了,Tx2 即使在 Tx1 全局未完成时,也能读到 Tx1 修改后的数据。

如果业务需要全局读已提交(Global Read Committed),必须使用 SELECT ... FOR UPDATE 配合 @GlobalLock 或 @GlobalTransactional。

Seata 代理增强逻辑如下:

  1. 解析到 SELECT ... FOR UPDATE。

  2. 查询是否持有全局锁。

  3. 如果有全局锁(说明有未完成的全局写事务),则等待锁释放。

  4. 读取数据(此时读到的就是全局已提交的数据)。


5. 实际代码演示

5.1 Maven 依赖

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>

5.2 业务逻辑实现

一个电商下单流程:Order-Service 调用 Storage-Service 和 Account-Service。

Order Service (TM):

@Service public class OrderServiceImpl implements OrderService { @Autowired private StorageFeignClient storageFeignClient; @Autowired private AccountFeignClient accountFeignClient; @Autowired private OrderMapper orderMapper; // xid 会通过 Feign 拦截器自动透传 @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class) @Override public void create(Order order) { LOGGER.info("----->开始新建订单"); // 1. 本地创建订单 orderMapper.create(order); // 2. 远程扣减库存 LOGGER.info("----->订单微服务开始调用库存,做扣减"); storageFeignClient.decrease(order.getProductId(), order.getCount()); // 3. 远程扣减账户 LOGGER.info("----->订单微服务开始调用账户,做扣减"); accountFeignClient.decrease(order.getUserId(), order.getMoney()); // 模拟异常,测试回滚 // int i = 1 / 0; LOGGER.info("----->订单结束"); } }

Storage Service (RM):

@Service public class StorageServiceImpl implements StorageService { @Autowired private StorageMapper storageMapper; // 不需要 @GlobalTransactional,只需要普通的 @Transactional // Seata 的代理源会自动识别 XID 并加入事务 @Transactional @Override public void decrease(Long productId, Integer count) { LOGGER.info("------->库存微服务收到 XID: " + RootContext.getXID()); // 这里的 SQL 执行会被 Seata 代理,生成 UndoLog 并注册全局锁 storageMapper.decrease(productId, count); } }

6. 总结与权衡

优点

  1. 高性能:Phase 1 直接提交本地事务,不长时间占用数据库连接。

  2. 零侵入:不需要像 TCC 那样写三段式代码,开发效率高。

  3. 自动补偿:框架自动生成回滚 SQL。

缺点与风险

  1. 脏写风险:必须确保所有涉及该数据的写操作都经过 Seata 体系(使用 @GlobalLock),否则会产生无法回滚的脏数据。

  2. 性能损耗:由于需要解析 SQL、生成 Before/After Image、序列化 UndoLog,相比原生 JDBC 有一定性能损耗(约降低 30%-50% 的写吞吐)。

  3. 全局锁争抢:在热点数据场景下(如扣减秒杀库存),全局锁的竞争会导致严重的性能下降,此时不建议使用 AT 模式,建议使用 TCC 或 Redis 方案。

http://www.jsqmd.com/news/377706/

相关文章:

  • 2026年全国防爆墙厂家哪家有实力?可靠专业有保障 适配多行业应用场景 - 深度智识库
  • 鼠标运维日志
  • 【2026】 LLM 大模型系统学习指南 (64)
  • 【2026】 LLM 大模型系统学习指南 (65)
  • 2026年最新高低压开关柜厂家五大推荐:箱式变电站、电力变压器、电力工程、变频控制柜厂家精选 - 深度智识库
  • 闲置京东 e 卡别浪费!安全变现全攻略 - 团团收购物卡回收
  • “0.5ms–2.5ms”与代码中“1000–2000”的矛盾
  • IEDA工具总结笔记
  • 2026年汽车应急启动电源跨境供货商推荐:ODM定制与品牌出海新机遇 - 品牌2025
  • 梳理孝感新材略律师法律服务,破产申请与受理优势在哪 - 工业推荐榜
  • 2026年汽车电瓶设备跨境供货商推荐:聚焦高端智造与全球布局 - 品牌2025
  • 跨场景迁移:具身智能的鲁棒性挑战与突破
  • 春节档电影哪个口碑好?我今年更愿意把《惊蛰无声》当作“全家不踩雷”的首选备选(题材阵容合家观影) - SFMEDIA
  • 探讨上海工作服订做性价比,更上制服值得考虑吗? - 工业品牌热点
  • 【大数据毕设全套源码+文档】基于django+大数据技术的租房大数据可视化系统的设计与实现(丰富项目+远程调试+讲解+定制)
  • 2026年全国水泥发泡隔墙板厂家哪家权威?实力强劲且服务贴心 口碑在线实用指南 - 深度智识库
  • 2026成都考驾照五大优选 自动挡学车专业指南 西南驾培品牌实力盘点 - 深度智识库
  • 2026温岭宠物眼科专家推荐,专业守护宠物视力,狗狗体检/异宠/猫咪绝育/狗狗绝育/宠物腹腔镜绝育,宠物眼科专家哪家好 - 品牌推荐师
  • Outlook客户端出现了office文档不能预览的问题
  • 说说易斯拉国际物流基本信息,其在广州深圳等地的市场声誉好吗? - 工业品网
  • 【大数据毕设全套源码+文档】基于django+大数据技术+ Spark 的音乐数据分析的设计与实现(丰富项目+远程调试+讲解+定制)
  • 基于C# WinForm的休闲五子棋游戏实现
  • PE文件格式
  • 【python毕设源码分享】基于Python的媒体资源管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 【大数据毕设源码分享】基于Django+数据可视化的 Spark 的音乐数据分析(程序+文档+代码讲解+一条龙定制)
  • 【大数据毕设源码分享】基于Django+数据可视化的租房大数据可视化系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • ELF文件格式
  • 【python毕设源码分享】基于Python的采用人脸识别技术的课堂考勤管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 逆向工程入门
  • 【python毕设源码分享】基于Python的见花则喜线上花店管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)