一、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) 时:
- 先查询原始数据:SELECT * FROM stock WHERE id=1 → 得到 quantity=200
- 生成 undo 日志:{"beforeImage": {"quantity":200}, "afterImage": {"quantity":100}}
- 执行更新操作: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 锁表导致数据库性能下降,你会如何优化?
正确姿势:
- 缩小锁的范围:通过业务逻辑减少受影响的行数
- 调整隔离级别:使用 READ_COMMITTED 降低锁粒度
- 异步化处理:将非关键操作放到事务外执行
3. 第三个陷阱:幂等性的「薛定谔的猫」
在分布式事务中,幂等性是必须解决的问题。但 AT 模式的幂等性实现存在一个致命缺陷:
// 库存服务接口
public void decreaseStock(Long productId, Integer count) {
// 检查是否已经扣减过
if (isAlreadyDecreased(productId)) {
return;
}
// 扣减库存
stockDao.decrease(productId, count);
}
假设网络抖动导致第二阶段提交重试,此时 isAlreadyDecreased 方法可能返回错误结果,导致重复扣减库存。
面试官经典问题:为什么 Seata AT 模式的幂等性需要业务方自己实现?
核心原因:Seata 只能保证全局事务的最终一致性,但无法感知业务逻辑中的唯一性约束。比如商品订单号、支付流水号等业务主键,必须由业务方在代码中处理。
正确方案:
- 使用唯一索引防重:在数据库层面创建唯一索引
- 状态机控制:通过状态字段 (status) 避免重复操作
- 幂等性令牌:每次请求生成唯一令牌,服务端校验
二、面试官必问的「灵魂三问」及满分答案
1. 问题一:Seata AT 模式的隔离级别是怎样的?
错误答案:默认是 REPEATABLE_READ。
正确答案:
- 第一阶段:通过行级锁保证 READ_COMMITTED 隔离级别
- 第二阶段:提交后释放锁,可能出现幻读
- 最终一致性:通过全局事务协调器保证最终结果一致
进阶回答:可以对比 XA 模式的 SERIALIZABLE 隔离级别,说明 AT 模式在性能和一致性之间的权衡。
2. 问题二:如果第二阶段提交失败,Seata 如何处理?
错误答案:会自动重试直到成功。
正确答案:
- 事务协调器 (TC) 会记录事务状态
- 定期扫描未完成的事务
- 对未提交的事务执行回滚
- 对未回滚的事务执行补偿操作
面试官追问:补偿操作如何实现?
- 答:通过业务方提供的 @Compensable 注解方法,执行反向操作。
3. 问题三:AT 模式与 TCC 模式的区别是什么?
送分题答案:
- AT 模式:无侵入性,自动生成 undo 日志
- TCC 模式:需要业务方实现 Try-Confirm-Cancel 接口
- 适用场景:AT 适合简单业务,TCC 适合复杂业务
加分回答:可以提到 Seata 的 Saga 模式,说明三者的适用场景差异。
三、避坑指南:Seata AT 模式的「三不要」原则
- 不要在事务中操作大表:比如一次更新百万级数据
- 不要忽略锁超时:合理设置 lockRetryTimeout 参数
- 不要完全依赖自动回滚:复杂业务需要手动补偿逻辑
案例分享:某电商公司曾因在 AT 事务中操作商品评论表(日均百万级更新),导致数据库锁竞争激烈,最终改用 TCC 模式+消息队列异步处理。
四、总结:分布式事务的「渡劫指南」
Seata AT 模式就像一把双刃剑,既能帮你解决分布式事务难题,也可能在关键时刻给你致命一击。掌握这三个致命陷阱的本质,不仅能应对面试,更能在实际项目中避免「埋雷」。