Seata AT 模式解析分布式事务面试必问的三个致命陷阱

96 阅读5分钟

一、AT 模式的「三层伪装」与致命缺陷

1. 第一个陷阱:undo 日志的「时光机」悖论

很多同学对 AT 模式的理解停留在「自动生成 undo 日志」的层面,但你知道 undo 日志的生成时机可能引发数据不一致吗?

举个栗子:

// 业务代码示例
@GlobalTransactional
public void createOrder() {
   // 1. 扣减库存
   stockDao.decrease(100);
   // 2. 创建订单
   orderDao.insert(new Order());
}

在 AT 模式下,Seata 会在执行 SQL 前生成 undo 日志。假设库存表有 200 件,执行 decrease(100) 时:

  1. 先查询原始数据:SELECT * FROM stock WHERE id=1 → 得到 quantity=200
  2. 生成 undo 日志:{"beforeImage": {"quantity":200}, "afterImage": {"quantity":100}}
  3. 执行更新操作:UPDATE stock SET quantity=100 WHERE id=1

陷阱点:如果在生成 undo 日志之后、执行 SQL 之前,其他事务修改了这条记录,会发生什么? 

比如另一个事务同时执行 UPDATE stock SET quantity=150 WHERE id=1,此时原始数据已经不是 200 了。Seata 回滚时会使用错误的 beforeImage 进行回滚,导致数据错乱。这就是传说中的「脏读」问题。

解决方案:Seata 通过 行级锁 来避免这种情况。在生成 undo 日志时,会先对记录加锁,确保在同一个全局事务中,其他事务无法修改该记录。但这又引发了第二个陷阱…

2. 第二个陷阱:锁的「贪吃蛇」效应

AT 模式的锁机制看似完美,但实际应用中可能引发性能灾难。比如下面这个场景:

-- 订单表
CREATE TABLE t_order (
   id BIGINT PRIMARY KEY,
   user_id BIGINT,
   status VARCHAR(20)
);
-- 扣减库存操作
UPDATE t_stock SET quantity = quantity - 1 WHERE product_id = 100;

当多个全局事务同时操作同一行数据时,Seata 会对 product_id=100 这一行加锁。但如果业务逻辑中存在范围查询,比如:

UPDATE t_order SET status = 'PAID' WHERE user_id = 100 AND status = 'NEW';

此时 Seata 会扫描所有符合条件的记录,并对每一行加锁。如果有 10 万条记录符合条件,就会产生 10 万个锁,导致性能急剧下降。

面试官灵魂拷问:如果 Seata 锁表导致数据库性能下降,你会如何优化?

正确姿势:

  1. 缩小锁的范围:通过业务逻辑减少受影响的行数
  2. 调整隔离级别:使用 READ_COMMITTED 降低锁粒度
  3. 异步化处理:将非关键操作放到事务外执行

3. 第三个陷阱:幂等性的「薛定谔的猫」

在分布式事务中,幂等性是必须解决的问题。但 AT 模式的幂等性实现存在一个致命缺陷:

// 库存服务接口
public void decreaseStock(Long productId, Integer count) {
   // 检查是否已经扣减过
   if (isAlreadyDecreased(productId)) {
       return;
   }
   // 扣减库存
   stockDao.decrease(productId, count);
}

假设网络抖动导致第二阶段提交重试,此时 isAlreadyDecreased 方法可能返回错误结果,导致重复扣减库存。

面试官经典问题:为什么 Seata AT 模式的幂等性需要业务方自己实现?

核心原因:Seata 只能保证全局事务的最终一致性,但无法感知业务逻辑中的唯一性约束。比如商品订单号、支付流水号等业务主键,必须由业务方在代码中处理。

正确方案:

  1. 使用唯一索引防重:在数据库层面创建唯一索引
  2. 状态机控制:通过状态字段 (status) 避免重复操作
  3. 幂等性令牌:每次请求生成唯一令牌,服务端校验

二、面试官必问的「灵魂三问」及满分答案

1. 问题一:Seata AT 模式的隔离级别是怎样的?

错误答案:默认是 REPEATABLE_READ。

正确答案:

  • 第一阶段:通过行级锁保证 READ_COMMITTED 隔离级别
  • 第二阶段:提交后释放锁,可能出现幻读
  • 最终一致性:通过全局事务协调器保证最终结果一致

进阶回答:可以对比 XA 模式的 SERIALIZABLE 隔离级别,说明 AT 模式在性能和一致性之间的权衡。

2. 问题二:如果第二阶段提交失败,Seata 如何处理?

错误答案:会自动重试直到成功。

正确答案:

  1. 事务协调器 (TC) 会记录事务状态
  2. 定期扫描未完成的事务
  3. 对未提交的事务执行回滚
  4. 对未回滚的事务执行补偿操作

面试官追问:补偿操作如何实现?

  • 答:通过业务方提供的 @Compensable 注解方法,执行反向操作。

3. 问题三:AT 模式与 TCC 模式的区别是什么?

送分题答案:

  • AT 模式:无侵入性,自动生成 undo 日志
  • TCC 模式:需要业务方实现 Try-Confirm-Cancel 接口
  • 适用场景:AT 适合简单业务,TCC 适合复杂业务

加分回答:可以提到 Seata 的 Saga 模式,说明三者的适用场景差异。

三、避坑指南:Seata AT 模式的「三不要」原则

  1. 不要在事务中操作大表:比如一次更新百万级数据
  2. 不要忽略锁超时:合理设置 lockRetryTimeout 参数
  3. 不要完全依赖自动回滚:复杂业务需要手动补偿逻辑

案例分享:某电商公司曾因在 AT 事务中操作商品评论表(日均百万级更新),导致数据库锁竞争激烈,最终改用 TCC 模式+消息队列异步处理。

四、总结:分布式事务的「渡劫指南」

Seata AT 模式就像一把双刃剑,既能帮你解决分布式事务难题,也可能在关键时刻给你致命一击。掌握这三个致命陷阱的本质,不仅能应对面试,更能在实际项目中避免「埋雷」。