高阶题 · 架构设计
1 题 · 平均 3.5/5 · 高阶题
**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 表结构(核心)**:
```sql
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)
);
```
**全局锁机制**:
- AT 保证写隔离靠**全局锁**(GlobalLock):同一行数据的全局事务在二阶段提交前,其他全局事务不能更新该行
- 实现:默认基于数据库行锁(`SELECT ... FOR UPDATE`)或本地锁;冲突超时抛 **`GlobalLockConflict`** 异常
- 注意:AT 不保证读隔离(读未提交),因此有短暂中间态——这是 AT 与 XA 隔离性的本质差异
| 维度 | 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 一阶段入口)**:
```java
@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 原子防超卖)**:
```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. 本地消息表(最终一致)**:
```sql
-- 扣减记录写入本地消息表(与业务同库事务)
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 只扣一次
);
```
```java
// 异步批量消费,幂等落库
@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 下必然瓶颈,需配合缓存化或换模式。
---