主题
十四、分布式事务(8 题)
14.1 ★★★★CAP 理论和 BASE 理论分别是什么?分布式事务产生的根本原因是什么?
📖 点击查看答案
CAP 理论:
- C(Consistency 一致性):所有节点数据一致,读操作总能读到最新写
- A(Availability 可用性):每个请求都能收到非错误响应(不保证最新)
- P(Partition Tolerance 分区容错):网络分区时系统仍能工作
- 核心结论:分布式系统只能满足 CAP 中的两个,由于 P 必选,故在 CP 和 AP 间权衡
- CP:ZooKeeper、etcd、HBase(强一致,分区时不可用)
- AP:Eureka、Cassandra、Redis 集群(高可用,允许最终一致)
BASE 理论(CAP 的 AP 延伸):
- B(Basically Available 基本可用):故障时允许损失部分可用性(响应时间增加、降级)
- S(Soft state 软状态):允许中间状态存在,不要求时时一致
- E(Eventually consistent 最终一致性):系统保证最终达到一致状态
分布式事务产生的根本原因:
- 数据分散存储:单机 ACID 事务无法覆盖跨库/跨服务的数据操作
- 微服务拆分:一个业务操作涉及多个服务,每个服务有独立数据库
- 网络不可靠:节点间通信可能失败,导致部分成功部分失败
- 缺乏全局协调:本地事务无法感知其他参与者状态,无法统一提交/回滚
java
// 典型场景:电商下单
// 订单服务(order_db):插入订单
// 库存服务(stock_db):扣减库存
// 账户服务(account_db):扣减余额
// 三者跨库跨服务,需要分布式事务保证一致性
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 本地事务
stockFeignClient.deduct(order); // 远程调用 - 可能失败
accountFeignClient.debit(order); // 远程调用 - 可能失败
// 若 accountFeignClient 失败,订单已生成、库存已扣减 → 数据不一致
}
}最终一致性方案演进:
- 强一致(2PC/XA)→ 性能差
- 柔性事务(TCC/Saga)→ 业务侵入
- 最终一致(本地消息表/事务消息)→ 异步、解耦
14.2 ★★★★2PC(两阶段提交)协议的流程是怎样的?它有哪些缺陷?3PC 相比 2PC 有哪些改进?
📖 点击查看答案
2PC(两阶段提交)流程:
阶段一:准备阶段(Prepare)
- 协调者发送
prepare请求给所有参与者 - 参与者执行事务操作(写 undo/redo log),不提交
- 参与者反馈
yes(可提交)或no(回滚) - 协调者等待所有参与者响应(超时未响应视为 no)
阶段二:提交/回滚阶段(Commit/Rollback)
- 所有参与者都
yes:协调者发送commit,参与者提交并释放资源 - 任一参与者
no或超时:协调者发送rollback,参与者回滚
java
// 伪代码示意
class Coordinator {
void twoPhaseCommit() {
// Phase 1
boolean allYes = true;
for (Participant p : participants) {
if (!p.prepare()) { allYes = false; break; }
}
// Phase 2
for (Participant p : participants) {
if (allYes) p.commit();
else p.rollback();
}
}
}2PC 缺陷:
- 同步阻塞:参与者锁定资源等待协调者,期间阻塞其他事务
- 单点故障:协调者宕机,参与者一直阻塞
- 数据不一致:Commit 阶段部分参与者收到 commit,部分未收到(网络分区)
- 性能差:两轮 RPC + 资源锁定,TPS 低
- 容错性差:任一参与者故障导致整个事务阻塞
3PC(三阶段提交)改进:
| 阶段 | 3PC 说明 |
|---|---|
| CanCommit | 询问是否可提交,不锁定资源(轻量级 check) |
| PreCommit | 执行事务,写日志,准备提交(相当于 2PC 的 prepare) |
| DoCommit | 真正提交 |
3PC 改进点:
- 引入超时机制:参与者超时后自动提交/回滚,减少阻塞
- 参与者在 PreCommit 后未收到 DoCommit,超时自动 commit
- CanCommit 预检查:提前发现不可提交的情况,减少资源锁定时间
- 降低阻塞范围:失败时参与者能自行决策
3PC 仍未解决的问题:
- 数据不一致风险(DoCommit 网络分区,部分自动 commit 部分回滚)
- 网络分区下仍不安全
- 实际应用较少(复杂度高,性能提升有限)
14.3 ★★★★Seata 的 AT 模式原理是什么?它的 undo_log 机制是如何工作的?AT 模式和 TCC 模式的对比?
📖 点击查看答案
Seata AT 模式核心概念:
- TC(Transaction Coordinator):事务协调者,维护全局事务状态
- TM(Transaction Manager):事务管理器,开启/提交/回滚全局事务
- RM(Resource Manager):资源管理器,管理分支事务
AT 模式流程:
一阶段(业务执行):
- TM 向 TC 开启全局事务,获取 XID
- SQL 解析:拦截 SQL,生成 before image(修改前数据)
- 执行业务 SQL
- 生成 after image(修改后数据)
- 将 before/after image 存入
undo_log表(与业务 SQL 同一本地事务提交) - 向 TC 注册分支事务,加全局锁
- 本地事务提交,立即释放本地锁(这是 AT 性能高的关键)
二阶段提交:
- TC 通知各 RM 提交,RM 异步删除 undo_log(性能高)
二阶段回滚:
- RM 根据 undo_log 的 before image 生成反向 SQL
- 校验 after image 与当前数据是否一致(防止脏写)
- 执行反向 SQL 恢复数据
- 删除 undo_log
sql
-- undo_log 表结构
CREATE TABLE undo_log (
branch_id BIGINT,
xid VARCHAR(128),
context VARCHAR(128),
rollback_info LONGBLOB, -- 存储 before/after image
log_status INT,
log_created DATETIME,
log_modified DATETIME,
UNIQUE KEY ux_undo_log (xid, branch_id)
);java
// Seata AT 使用
@GlobalTransactional
public void purchase(String userId, String commodityCode, int count) {
storageFeignClient.deduct(commodityCode, count); // 分支事务1
orderFeignClient.create(userId, commodityCode, count); // 分支事务2
accountFeignClient.debit(userId, count * 100); // 分支事务3
// 任一失败,全局回滚,根据 undo_log 反向恢复
}AT vs TCC 对比:
| 维度 | AT 模式 | TCC 模式 |
|---|---|---|
| 侵入性 | 无(自动拦截 SQL) | 高(需手写 Try/Confirm/Cancel) |
| 一致性 | 最终一致(一阶段提交,存在脏读) | 最终一致(业务隔离) |
| 性能 | 高(一阶段即提交) | 高(不依赖数据库事务) |
| 隔离性 | 弱(需全局锁辅助) | 业务层隔离 |
| 回滚方式 | 自动反向 SQL | 手写 Cancel 逻辑 |
| 适用场景 | 关系型数据库、简单 CRUD | 非数据库资源(如扣库存)、需强隔离 |
| 限制 | 需要数据库支持事务、有主键 | 业务复杂度高 |
AT 模式隔离性问题:
- 写隔离:通过全局锁保证(一阶段提交前获取全局锁)
- 读隔离:默认读未提交,需
@GlobalLock + SELECT FOR UPDATE实现读已提交
14.4 ★★★★TCC(Try-Confirm-Cancel)模式的原理是什么?如何保证空回滚、幂等、悬挂三个问题?
📖 点击查看答案
TCC 原理:
- Try:资源预留(如冻结余额、预占库存),不真正扣减
- Confirm:确认执行,真正扣减预留资源
- Cancel:取消执行,释放预留资源
java
// 账户服务 TCC 实现
public interface AccountTccAction {
@TwoPhaseBusinessAction(name = "debit", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDebit(BusinessActionContext ctx, String userId, BigDecimal amount);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
@Service
public class AccountTccActionImpl implements AccountTccAction {
// Try:冻结金额
public boolean tryDebit(BusinessActionContext ctx, String userId, BigDecimal amount) {
// 检查幂等性(事务记录表)
if (exists(ctx.getXid(), ctx.getBranchId())) return true;
// 检查并冻结
accountMapper.freeze(userId, amount); // available -= amount, frozen += amount
// 记录事务日志
insertTccLog(ctx.getXid(), ctx.getBranchId(), "TRY");
return true;
}
// Confirm:扣减冻结金额
public boolean confirm(BusinessActionContext ctx) {
if (exists(ctx.getXid(), ctx.getBranchId())) return true; // 幂等
accountMapper.deductFrozen(ctx.getActionContext("userId"), ctx.getActionContext("amount"));
updateTccLog(ctx.getXid(), ctx.getBranchId(), "CONFIRM");
return true;
}
// Cancel:解冻
public boolean cancel(BusinessActionContext ctx) {
// 空回滚检查:若 Try 未执行,直接插入标记
if (!exists(ctx.getXid(), ctx.getBranchId())) {
insertTccLog(ctx.getXid(), ctx.getBranchId(), "CANCEL_EMPTY");
return true;
}
// 悬挂检查:若已 Cancel,不再执行
if (isCancelled(ctx.getXid(), ctx.getBranchId())) return true;
accountMapper.unfreeze(ctx.getActionContext("userId"), ctx.getActionContext("amount"));
updateTccLog(ctx.getXid(), ctx.getBranchId(), "CANCEL");
return true;
}
}三大问题及解决方案:
1. 空回滚:
- 问题:Try 未执行(如超时/网络问题),但 Cancel 被调用
- 解决:Cancel 前检查 Try 是否执行(查事务记录表),未执行则插入"空回滚"标记直接返回
2. 幂等性:
- 问题:Confirm/Cancel 可能被重试调用
- 解决:事务记录表记录状态(TRY/CONFIRM/CANCEL),执行前检查状态,已执行则直接返回
3. 悬挂:
- 问题:Cancel 比 Try 先执行(如 Try 超时后 Cancel 触发,延迟的 Try 才到达)
- 解决:Try 前检查是否已 Cancel,若已 Cancel 则不再执行 Try
sql
-- TCC 事务记录表
CREATE TABLE tcc_transaction_log (
xid VARCHAR(128) PRIMARY KEY,
branch_id VARCHAR(128),
status VARCHAR(20), -- TRY / CONFIRM / CANCEL / CANCEL_EMPTY
business_key VARCHAR(128),
created_time DATETIME
);TCC 适用场景:
- 高性能要求、需要强隔离
- 非数据库资源(如库存、积分)
- 资金类业务(需保证不超扣)
14.5 ★★★Saga 模式的工作原理是什么?适合什么场景?补偿事务如何设计?
📖 点击查看答案
Saga 模式原理:
- 将长事务拆分为多个本地事务 T1, T2, ..., Tn
- 每个本地事务 Ti 对应一个补偿事务 Ci
- 正向执行:T1 → T2 → ... → Tn
- 回滚执行:若 Tk 失败,执行 Ck-1 → Ck-2 → ... → C1
两种实现方式:
1. 编排式(Choreography):去中心化,各服务监听事件触发下一步
java
// 订单服务
public void createOrder() {
orderMapper.insert(order);
eventBus.publish(new OrderCreatedEvent(order)); // 发布事件
}
// 库存服务监听
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
stockMapper.deduct(event.getOrderId());
eventBus.publish(new StockDeductedEvent(event.getOrderId()));
}
// 支付服务监听
@EventListener
public void onStockDeducted(StockDeductedEvent event) {
try {
payMapper.pay(event.getOrderId());
} catch (Exception e) {
eventBus.publish(new PayFailedEvent(event.getOrderId())); // 触发补偿
}
}2. 协调式(Orchestration):中心化协调器管理流程
java
class SagaOrchestrator {
void execute(Order order) {
try {
executeStep(() -> orderService.create(order));
executeStep(() -> stockService.deduct(order));
executeStep(() -> payService.pay(order));
executeStep(() -> deliveryService.ship(order));
} catch (Exception e) {
compensate(order); // 反向补偿
}
}
void compensate(Order order) {
tryStep(() -> deliveryService.cancelShip(order));
tryStep(() -> payService.refund(order));
tryStep(() -> stockService.restore(order));
tryStep(() -> orderService.cancel(order));
}
}补偿事务设计要点:
- 补偿必须是幂等的:可能被重试
- 补偿不能失败:需重试机制 + 人工兜底
- 补偿语义明确:如支付补偿是退款,不是删除支付记录
- 不可补偿的操作放最后:如发送邮件、发货等
- 隔离性问题:中间状态可见,需业务容忍或加锁
Saga 适用场景:
- 长流程业务(旅游预订、订单履约)
- 涉及第三方系统(无法 XA)
- 微服务编排
- 业务可容忍中间不一致
Saga vs TCC:
- Saga:无 Try 阶段,直接执行真实操作,失败补偿,适合长流程
- TCC:Try 预留资源,性能高但侵入性强,适合短事务
14.6 ★★★基于本地消息表的最终一致性方案是如何实现的?适用什么场景?
📖 点击查看答案
核心思想:利用本地事务保证业务操作和消息记录原子性,异步发送消息保证最终一致性。
实现流程:
- 业务操作 + 插入本地消息表(同一本地事务)
- 定时任务扫描消息表中"未发送"的消息
- 调用下游服务(或投递 MQ)
- 下游消费成功后回调更新消息状态
- 失败重试,超过次数告警人工介入
java
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// 同一事务插入消息表
Message msg = new Message();
msg.setBizId(order.getId());
msg.setType("ORDER_CREATED");
msg.setPayload(JSON.toJSONString(order));
msg.setStatus("PENDING");
msg.setRetryCount(0);
messageMapper.insert(msg); // 与业务同一事务
}
}
// 定时任务扫描发送
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<Message> msgs = messageMapper.selectByStatus("PENDING", 100);
for (Message msg : msgs) {
try {
mqProducer.send(msg.getTopic(), msg.getPayload());
messageMapper.updateStatus(msg.getId(), "SENT");
} catch (Exception e) {
messageMapper.incrementRetry(msg.getId());
if (msg.getRetryCount() > MAX_RETRY) {
messageMapper.updateStatus(msg.getId(), "FAILED");
alertService.alert("消息发送失败: " + msg.getId());
}
}
}
}
// 消费端幂等处理
@RocketMQMessageListener(topic = "ORDER_CREATED")
public class OrderConsumer implements RocketMQListener<String> {
public void onMessage(String payload) {
OrderEvent event = JSON.parseObject(payload, OrderEvent.class);
if (exists(event.getBizId())) return; // 幂等检查
process(event);
}
}表设计:
sql
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64),
topic VARCHAR(100),
payload TEXT,
status VARCHAR(20), -- PENDING / SENT / CONFIRMED / FAILED
retry_count INT DEFAULT 0,
next_retry DATETIME,
created_time DATETIME,
updated_time DATETIME,
INDEX idx_status_retry (status, next_retry)
);适用场景:
- 异步解耦的最终一致性场景
- 订单创建后通知积分、优惠券、物流
- 对实时性要求不高的跨服务数据同步
优缺点:
- 优点:实现简单、可靠性高、不依赖 MQ 事务
- 缺点:与业务耦合(消息表侵入)、定时扫描有延迟、高并发下消息表压力大
14.7 ★★★基于 RocketMQ 事务消息实现最终一致性的方案是如何工作的?与本地消息表方案相比有什么优劣?
📖 点击查看答案
RocketMQ 事务消息原理:利用半消息(Half Message)+ 事务回查机制保证本地事务与消息发送的原子性。
核心流程:
- 发送半消息:Producer 发送半消息到 Broker,半消息对消费者不可见
- 执行本地事务:Producer 执行本地业务事务
- 提交/回滚:
- 本地事务成功 → 发送 Commit,半消息变为可消费
- 本地事务失败 → 发送 Rollback,删除半消息
- 事务回查:若 Broker 未收到 Commit/Rollback(如网络问题),主动回调 Producer 的
checkLocalTransaction查询本地事务状态 - 消费:消费者消费消息,幂等处理
java
// 事务消息生产者
public class OrderTransactionProducer {
public void sendOrderMessage(Order order) {
Message msg = new Message("ORDER_TOPIC", JSON.toJSONString(order).getBytes());
TransactionMQProducer producer = new TransactionMQProducer("order_group");
producer.setTransactionListener(new TransactionListener() {
// 执行本地事务
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
orderService.createOrder(order); // 本地事务
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 事务回查(Broker 主动调用)
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Order order = JSON.parseObject(msg.getBody(), Order.class);
return orderService.exists(order.getId())
? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.ROLLBACK_MESSAGE;
}
});
producer.sendMessageInTransaction(msg, null);
}
}与本地消息表对比:
| 维度 | RocketMQ 事务消息 | 本地消息表 |
|---|---|---|
| 业务侵入 | 低(接口回调) | 高(需建消息表) |
| 实现复杂度 | 中(需实现回查) | 低(定时任务扫描) |
| 实时性 | 高(事务提交即发送) | 低(定时扫描有延迟) |
| 数据库压力 | 无 | 消息表压力大 |
| 依赖 | 强依赖 RocketMQ | 任何 MQ 即可 |
| 回查机制 | 内置 | 需自己实现重试 |
| 适用场景 | RocketMQ 技术栈 | 异构 MQ、老系统改造 |
注意事项:
- 事务消息只保证"本地事务与消息发送"的一致性,不保证消费成功(消费端需幂等)
- 事务回查有次数限制(默认 15 次),超时后按规则处理
- 半消息有存储成本,不适合高频小消息
14.8 ★★★各分布式事务方案如何选型(强一致 vs 最终一致)?Seata 的 AT、TCC、Saga、XA 四种模式分别适用于什么场景?
📖 点击查看答案
强一致 vs 最终一致选型:
| 维度 | 强一致(XA/2PC) | 最终一致(TCC/Saga/消息) |
|---|---|---|
| 性能 | 低(资源锁定) | 高 |
| 可用性 | 低(阻塞) | 高 |
| 一致性 | 强 | 最终 |
| 适用场景 | 资金、库存核心交易 | 互联网高并发业务 |
| 业务侵入 | 低 | 中-高 |
选型决策树:
- 是否涉及第三方系统?是 → Saga / 事务消息
- 是否为资金类强一致场景?是 → TCC / XA
- 是否关系型数据库 + 简单 CRUD?是 → Seata AT
- 是否长流程业务?是 → Saga
- 是否需要异步解耦?是 → 本地消息表 / 事务消息
Seata 四种模式适用场景:
| 模式 | 一致性 | 侵入性 | 性能 | 适用场景 |
|---|---|---|---|---|
| AT | 最终一致 | 无(自动) | 高 | 关系型数据库、简单 CRUD、快速接入 |
| TCC | 最终一致 | 高(手写三阶段) | 高 | 资金/库存扣减、需强隔离、非 DB 资源 |
| Saga | 最终一致 | 中(补偿) | 中 | 长流程业务编排、第三方系统、旅游预订 |
| XA | 强一致 | 无 | 低 | 跨库强一致、并发低、金融核心 |
java
// Seata 模式选择示例
// AT:最常用,无侵入
@GlobalTransactional
public void transfer(Long from, Long to, BigDecimal amount) {
accountMapper.debit(from, amount);
accountMapper.credit(to, amount);
}
// TCC:高并发资金场景
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean tryDeduct(BusinessActionContext ctx, String account, BigDecimal amount) {
accountMapper.freeze(account, amount); // 冻结
return true;
}
// Saga:长流程编排
@SagaOrchchestration
public void bookTrip(TripOrder order) {
step(bookFlight).compensate(cancelFlight);
step(bookHotel).compensate(cancelHotel);
step(bookCar).compensate(cancelCar);
step(payment).compensate(refund);
}
// XA:跨库强一致
@GlobalTransactional(transactionType = GlobalTransactionType.XA)
public void crossDbUpdate() {
db1Mapper.update(...); // xa_db1
db2Mapper.update(...); // xa_db2
}实战建议:
- 优先 AT 模式(零侵入),满足 80% 场景
- 资金/库存场景用 TCC(隔离性好)
- 长流程编排用 Saga
- XA 仅在强一致刚需时使用
- 异步场景用本地消息表/事务消息
- 同一系统可混合使用多种模式