日期:2026-10-27
总题数:1 题(高阶单题)
方向:架构设计 · Seata AT 分布式事务
场景:电商下单链路,订单服务 + 库存服务 + 积分服务三个微服务,一次下单跨 3 个服务、更新 4 张表(order 主表、order_item、库存表、积分账户表),要求数据最终一致。
场景细节:
GlobalLockConflict 异常3 个子问题:
Q1:at 模式的工作原理是基于 seata 内部日志表的自动化事务,类似 2PC 但是也和 TCC 有些接近,会提交本地事务,但是回滚是基于 seata 的日志表去回滚的,有中间状态;业务侧提交全局事务,然后事务协调者创建全局事务,阻塞同行数据的事务操作;如果业务侧执行没有问题,那么就整体提交,如果业务侧出现问题,则根据 RM 记录的日志进行回滚,反向的数据操作,把数据回滚。 Q2:AT 模式没什么侵入性,使用全局锁做单行数据的隔离,性能上其实还好,整个过程中只会阻塞单行数据的操作,适合在不想做太多改造,一致性有一定要求但是能接受短暂脏状态的场景;TCC 模式侵入性比较强,隔离性方面需要业务侧自己实现,TCC 本身没有隔离性的实现,性能方面 TCC 全程没有什么锁,性能很好,各个服务都是自己的本地事务不用相互影响;适用于跨服务操作,多源数据无法使用基于数据库的 XA 和 AT 模式的场景,并且对一致性要求不高但是性能要求高的场景;XA 模式侵入性也不高,隔离性完全基于数据库的事务算是最好的,但是性能相对也是最低的,提交过程中完全阻塞,适合同库下的分布式事务,金融行业等要求一致性高的场景。 Q3:库存热点 AT 模式变慢的核心原因是 AT 模式存在全局锁,会在更新单个 sku 的库存的时候产生阻塞,导致业务请求串行;优化方面可以考虑库存转到缓存中,并且通过事务消息,将扣减记录通过消息记录下来,然后周期性的批量消费,执行批量扣减库存操作,提升整体效率;AT 可以转成 TCC 或者还是用 AT 模式,但是把库存扣减服务转为事务消息异步去执行批量扣减,中间的数据一致性和库存控制通过缓存来进行库存扣减脱离数据库。
评分:⭐⭐⭐⭐☆(3.5/5)
优点:
不足:
@GlobalTransactional 注解 + 代理拦截,自动生成 undo_log、自动补偿 SQL;说 AT「和 TCC 有些接近」表述模糊(两者都两阶段但机制完全不同)GlobalLockConflict 直接机制(AT 全局锁默认走数据库锁或本地锁,冲突即抛)、批量扣减需补幂等、可加"缩小 @GlobalTransactional 粒度"补充:Q3 的 Redis 预扣减(Lua 原子防超卖)+ 本地消息表/事务消息 + 异步批量落库 + 对账是热点库存标准解法。
Q1 · Seata AT 工作原理
【结论】面试官您好,Seata AT 是两阶段自动补偿方案,基于 undo_log 反向 SQL 回滚,业务代码零侵入。
【三点】第一,三组件——TC(事务协调者,管理全局事务状态)、TM(发起全局事务,@GlobalTransactional 标注)、RM(管理分支事务资源,注册分支 + 提交/回滚);第二,一阶段——代理拦截执行业务 SQL,自动生成 undo_log(beforeImage/afterImage 快照)、注册分支事务,然后本地事务先提交(不锁全局资源);第三,二阶段——全局提交则异步删除 undo_log,全局回滚则根据 undo_log 反向执行补偿 SQL,恢复数据。
【边界】AT 的隔离是"读未提交 + 全局锁写隔离",存在短暂中间态(脏读可能);热点高冲突场景下全局锁是性能瓶颈。
Q2 · AT / TCC / XA 对比
【结论】面试官您好,三种模式的核心差异在"业务侵入性、隔离性、性能"的权衡。
【三点】第一,XA——数据库级两阶段,隔离性最好(完全基于数据库事务),但性能最低(两阶段阻塞 + 同步提交),跨数据源场景、金融等高一致场景;第二,TCC——业务需手写 try/confirm/cancel,try 阶段冻结/预留资源,全程无锁性能好,适合高性能跨服务、无法用 XA/AT 的多源场景;第三,AT——自动 undo_log,业务无侵入,全局锁做单行写隔离,适合不想大改造、可接受短暂脏状态。
【边界】XA 阻塞性能低;TCC 开发成本高(try/confirm/cancel 三方法);AT 在热点高冲突下全局锁是瓶颈——选型看"一致性要求 vs 性能要求 vs 改造成本"。
Q3 · 库存热点优化
【结论】面试官您好,热点库存的核心是把扣减从数据库全局锁里解脱出来——Redis 预扣减 + 异步批量落库,或转 TCC try 冻结。
【三点】第一,库存预热到 Redis,用 Lua 原子预扣减(DECR 防超卖),扣减不碰 DB 全局锁;第二,扣减记录通过本地消息表/事务消息异步消费,批量落库(DB 最终一致);第三,缩小 @GlobalTransactional 粒度——只包必要的扣减分支,不包整个下单链路。
【边界】批量扣减必须幂等(消息去重,防止重复扣减);缓存与 DB 最终一致靠对账兜底;Redis 宕机时降级回 DB 扣减(保留兜底库存)。
三组件协作:
| 组件 | 角色 | 职责 |
|---|---|---|
| TC(Transaction Coordinator) | 事务协调者 | 维护全局/分支事务状态,驱动全局提交或回滚 |
| TM(Transaction Manager) | 事务管理器 | @GlobalTransactional 发起全局事务,告诉 TC 开启/提交/回滚 |
| RM(Resource Manager) | 资源管理器 | 管理分支事务:注册分支、提交/回滚本地事务、上报状态 |
两阶段(AT):
一阶段(本地事务提交,不锁全局)
@GlobalTransactional 代理拦截
→ 执行业务 SQL(如 UPDATE stock SET stock=stock-1)
→ 自动生成 undo_log(beforeImage / afterImage 快照)
→ 注册分支事务到 TC
→ 提交本地事务(此时数据已变更但未全局确认)
二阶段(全局决定)
全局提交 → 异步删除 undo_log(数据已生效,无需恢复)
全局回滚 → 根据 undo_log 反向执行补偿 SQL(beforeImage 恢复数据)
undo_log 表结构(核心):
CREATE TABLE undo_log (
id BIGINT NOT NULL AUTO_INCREMENT,
branch_id BIGINT NOT NULL, -- 分支事务 ID
xid VARCHAR(128) NOT NULL, -- 全局事务 ID
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL, -- beforeImage/afterImage 快照(JSON)
log_status INT NOT NULL, -- 0 正常 / 1 已回滚
log_created DATETIME,
log_modified DATETIME,
PRIMARY KEY (id),
UNIQUE KEY uk_undo_log (xid, branch_id)
);
全局锁机制:
SELECT ... FOR UPDATE)或本地锁;冲突超时抛 GlobalLockConflict 异常| 维度 | XA | AT | TCC |
|---|---|---|---|
| 业务侵入性 | 低(@Transactional 增强) | 低(@GlobalTransactional 自动) | 高(手写 try/confirm/cancel) |
| 隔离性 | 最好(数据库级) | 写隔离(全局锁)/ 读未提交 | 需业务实现(try 冻结) |
| 性能 | 最低(两阶段阻塞+同步) | 中(单行全局锁) | 最高(全程无锁) |
| 回滚机制 | 数据库自动回滚 | undo_log 反向 SQL | 业务 cancel 补偿 |
| 数据源 | 跨数据源(TM 协调多 RM) | 跨数据源 | 跨数据源 |
| 典型场景 | 金融、高一致 | 不想大改造、可接受短暂脏状态 | 高性能跨服务、多源无法用 XA/AT |
选型决策:
一致性要求极高(金融)→ XA
一致性要求高 + 不想改造 → AT
高性能 + 业务可拆分三阶段 → TCC
热点高冲突(单行 2000 QPS)→ AT 会瓶颈,转 TCC 或缓存化
1. @GlobalTransactional(AT 一阶段入口):
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 三个分支事务:各自提交本地事务,但全局未确认
orderService.createOrder(dto); // 分支 1
stockService.deductStock(dto); // 分支 2(热点扣减)
pointsService.addPoints(dto); // 分支 3
// 方法正常返回 → TM 通知 TC 全局提交 → 删除 undo_log
}
2. 热点库存 Redis 预扣减(Lua 原子防超卖):
-- 预扣减库存,返回剩余库存;不足返回 -1
local stock = redis.call('GET', KEYS[1])
if not stock then return -2 end
if tonumber(stock) <= 0 then return -1 end
redis.call('DECR', KEYS[1])
return tonumber(stock) - 1
3. 本地消息表(最终一致):
-- 扣减记录写入本地消息表(与业务同库事务)
CREATE TABLE stock_deduct_msg (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
quantity INT NOT NULL,
status TINYINT DEFAULT 0, -- 0 待消费 / 1 已消费
create_time DATETIME,
UNIQUE KEY uk_order_sku (order_id, sku_id) -- 幂等:同一订单同一 SKU 只扣一次
);
// 异步批量消费,幂等落库
@Scheduled(fixedDelay = 5000)
public void batchDeduct() {
List<StockDeductMsg> msgs = mapper.selectPending(100);
for (StockDeductMsg msg : msgs) {
// 幂等:UPDATE stock SET stock=stock-? WHERE sku_id=? AND stock>=?
int rows = stockMapper.deductIfEnough(msg.getSkuId(), msg.getQuantity());
if (rows == 1) {
mapper.markConsumed(msg.getId()); // 消费成功标记
} else {
alert(msg); // 库存不足告警,人工介入
}
}
}
| 方案 | 一致性 | 性能 | 复杂度 | 适用 |
|---|---|---|---|---|
| 纯 AT(现状) | 最终一致 | 低(全局锁串行) | 低 | 低冲突场景 |
| AT + Redis 预扣减 + 异步落库 | 最终一致(缓存优先) | 高(脱离 DB 锁) | 中 | 热点库存 |
| 转 TCC(try 冻结库存) | 最终一致(业务保证) | 高 | 高(三方法) | 高并发 + 可拆分 |
| 本地消息表(无全局事务) | 最终一致(消息驱动) | 高 | 中 | 非强一致链路 |
核心结论:热点库存的根因是 AT 全局锁串行化单 SKU 写,优化要么脱离数据库锁(Redis 预扣减 + 异步批量落库 + 幂等 + 对账),要么转 TCC 的 try 冻结(业务层控制库存预留)。纯 AT 在 2000 QPS 单 SKU 下必然瓶颈,需配合缓存化或换模式。
| 轮次 | 方向 | 难度 | 表现 | 得分 |
|---|---|---|---|---|
| R1 | 架构设计(Seata AT) | 高阶 | 主骨架对,Q1 缺代理机制灵魂、Q2 XA 概念偏差、Q3 落地性最好 | 3.5/5 |
distributed-transaction 域复习 Seata AT 源码级细节(undo_log 生成、全局锁、branch 注册)Q: 抽取一个高阶题目(架构设计 · Seata AT 分布式事务,跨 3 服务 4 张表,日均 50 万单,单 SKU 2000 QPS,AT 模式 RT 飙 800ms+ / GlobalLockConflict)。
A: (见上方"原始回答")
点评: (见上方"点评")均分 3.5/5,Q3 落地性最好,Q1 缺 AT 代理机制灵魂,Q2 XA"同库"概念偏差。