distributed-transactionlisted
Install: claude install-skill 1398281322-a11y/java-backend-guardrails
# 分布式事务
## When to Invoke
一次业务要改两个服务或两个库:下单后发券、支付成功后分佣、核销后结算、退款后回库存。
## When NOT
单服务单库:用本地 `@Transactional`。不要为了「微服务」上分布式事务。
## 风险(面试考点)
服务 A 成功、服务 B 失败 → 钱货不一致。面试常逼问 2PC/TCC/Seata。那些方案有锁时间和耦合代价。业务体量够用「本地事务 + 可靠消息」时上 TCC 就是过度设计。
## 适用场景
- 订单服务写订单,营销服务发券
- 支付成功通知分销结算
- 核销服务改券状态,订单服务改核销状态
## 方案选型(轻量优先)
| 优先级 | 方案 | 一致性 | 用在 | 不要用在 |
|--------|------|--------|------|----------|
| 1 | 本地事务 + RocketMQ 事务消息 | 最终 | 已有 RocketMQ,跨服务通知 | 需要同步强一致返回 |
| 2 | 本地消息表(outbox) | 最终 | 不能用事务消息时 | 愿意引入 Seata 之前 |
| 3 | Saga(成功路径 + 补偿) | 最终 | 长流程、可补偿 | 补偿写不清的资金流 |
| 4 | TCC | 近似强一致 | 预占资源必须 Confirm/Cancel | 默认业务 |
| 5 | Seata AT / XA | 强一致表象 | 明确要求且能接受锁 | 默认;长事务、高并发 |
默认:RocketMQ 事务消息(或 outbox)。禁止无脑 TCC/Seata。
升级到 TCC 的触发条件:必须预占(库存/余额冻结),Confirm/Cancel 有明确对账,团队能运维空回滚、悬挂。
## 默认方案(RocketMQ 事务消息)
本地 DB 提交与「半消息」绑定:本地成功 Commit 消息,本地失败 Rollback 消息。Broker 回查本地状态。
```java
transactionMQProducer.sendMessageInTransaction(msg, order);
// executeLocalTransaction: 写订单(本地事务)
// checkLocalTransaction: SELECT 订单是否已提交 → COMMIT / ROLLBACK
```
消费者(发券 / 分佣)必须幂等,见 `idempotent`。消息会重复。
Outbox 等价实现:
```sql
-- 与订单同一本地事务
INSERT INTO outbox(biz_no, topic, body, status) VALUES (?, ?, ?, 'NEW');
```
定时/CDC 扫描 `NEW` 发 RocketMQ,成功标 `SENT`。消费者幂等。
补偿(Saga)只在「能反向操作」时写:发券失败可关单;分佣失败可重试入账,不可随便再扣一次。
## 反例
错误:A 服务 HTTP 调 B,A 本地已提交,B 超时后 A 再重试却把超时当失败回滚——对端可能已成功。
正确:超时当未知;B 幂等;用消息而不是同步链。
错误:一上来 Seata 全局锁包下单+支付+分佣。
正确:支付回调已是异步,用事务消息即可。
错误:TCC 的 Cancel 不可幂等,悬挂时误取消别人的 Try。
正确:TCC 每个阶段都要幂等,用冻结单号关联。
## 验证
- 本地事务回滚,下游收