分布式事务完全指南
一、为什么需要分布式事务?
1.1 单体应用的事务
单体应用中,所有操作共享一个数据库连接,事务很简单:
java
@Transactional
public void createOrder(OrderRequest request) {
orderMapper.insert(order); // 操作1:插入订单
stockMapper.deduct(productId, qty); // 操作2:扣减库存
accountMapper.deduct(userId, amount); // 操作3:扣减余额
// 三个操作要么全部成功,要么全部回滚 → 一个 @Transactional 搞定
}
1.2 微服务架构的问题
当订单、库存、账户拆成三个独立服务后:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 订单服务 │ │ 库存服务 │ │ 账户服务 │
│ order_db │ │ stock_db │ │ account_db │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
↓ ↓ ↓
订单表 库存表 账户表
每个服务有自己的数据库,@Transactional 只能管本地事务,无法保证跨服务的一致性:
1. 订单服务:创建订单 ✅
2. 库存服务:扣减库存 ✅
3. 账户服务:扣减余额 ❌(余额不足)
结果:订单创建了,库存扣了,但钱没扣 → 数据不一致!
分布式事务就是要解决这个跨服务数据一致性问题。
二、理论基础
2.1 ACID vs BASE
| ACID(传统事务) | BASE(分布式系统) | |
|---|---|---|
| A | Atomicity(原子性) | Basically Available(基本可用) |
| C | Consistency(一致性) | Soft State(软状态) |
| I | Isolation(隔离性) | Eventually Consistent(最终一致性) |
| D | Durability(持久性) | --- |
| 适用 | 单库事务 | 分布式系统 |
| 核心思想 | 强一致性,全有或全无 | 允许短暂不一致,最终达到一致 |
2.2 CAP 定理
分布式系统中,以下三者不可能同时满足:
| 字母 | 含义 | 说明 |
|---|---|---|
| C | Consistency(一致性) | 所有节点同一时刻数据相同 |
| A | Availability(可用性) | 每个请求都能收到响应 |
| P | Partition Tolerance(分区容错) | 网络分区时系统仍能运行 |
微服务架构中网络分区不可避免(P 必须保证),所以只能在 C 和 A 之间取舍:
- CP:保证一致性,牺牲可用性(如 2PC)
- AP:保证可用性,牺牲强一致性(如最终一致性方案)
大多数互联网业务选择 AP + 最终一致性。
2.3 分布式事务方案谱系
强一致性 ←──────────────────────────────────────→ 最终一致性
(性能差,复杂度低) (性能好,复杂度高)
2PC/3PC │ TCC │ Saga │ 消息队列+补偿
(XA协议) │ (三阶段) │ (编排/协调) │ (最终一致性)
│ │ │
几乎不用 │ 金融核心 │ 电商/订单 │ 大部分业务
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、方案一:本地消息表 + 消息队列(最终一致性)
3.1 核心思想
将"跨服务调用"转化为"本地事务 + 异步消息",通过消息的可靠投递保证最终一致性。
3.2 工作流程
┌─── 订单服务 ──────────────────────────────────────┐
│ │
│ @Transactional(本地事务) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. INSERT INTO t_order (创建订单) │ │
│ │ 2. INSERT INTO t_local_message (写入本地消息表)│ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 定时任务:扫描本地消息表 → 发送到 MQ │
│ 确认发送成功 → 更新消息状态为"已发送" │
│ │
└───────────────────────────────────────────────────┘
│
↓ MQ(Kafka/RocketMQ)
│
┌─── 库存服务 ──────────────────────────────────────┐
│ │
│ 消费者接收消息 │
│ @Transactional(本地事务) │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 幂等检查(消息是否已处理过) │ │
│ │ 2. UPDATE stock SET qty = qty - N(扣减库存) │ │
│ │ 3. INSERT INTO t_consume_log(记录消费日志) │ │
│ └─────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘
3.3 完整代码示例
本地消息表(SQL)
sql
CREATE TABLE t_local_message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
message_id VARCHAR(64) NOT NULL UNIQUE COMMENT '消息唯一ID',
topic VARCHAR(128) NOT NULL COMMENT '目标Topic',
message_body TEXT NOT NULL COMMENT '消息内容(JSON)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待发送 1-已发送 2-发送失败',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
max_retry INT NOT NULL DEFAULT 5 COMMENT '最大重试次数',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_retry (status, retry_count)
) COMMENT '本地消息表';
消息实体
java
@Data
@TableName("t_local_message")
public class LocalMessage {
@TableId(type = IdType.AUTO)
private Long id;
private String messageId;
private String topic;
private String messageBody;
private Integer status; // 0-待发送 1-已发送 2-发送失败
private Integer retryCount;
private Integer maxRetry;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
订单服务 --- 创建订单(本地事务写入消息)
java
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final LocalMessageMapper localMessageMapper;
@Override
@Transactional // 本地事务:订单和消息在同一个事务中
public OrderResponse createOrder(CreateOrderRequest request) {
// 1. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setTotalAmount(request.getTotalAmount());
order.setStatus("CREATED");
orderMapper.insert(order);
// 2. 写入本地消息表(与订单在同一个事务中,要么都成功要么都回滚)
StockDeductMessage stockMsg = new StockDeductMessage();
stockMsg.setOrderNo(order.getOrderNo());
stockMsg.setProductId(request.getProductId());
stockMsg.setQuantity(request.getQuantity());
LocalMessage message = new LocalMessage();
message.setMessageId(UUID.randomUUID().toString());
message.setTopic("stock-deduct-topic");
message.setMessageBody(JsonUtil.toJson(stockMsg));
message.setStatus(0); // 待发送
message.setRetryCount(0);
message.setMaxRetry(5);
localMessageMapper.insert(message);
log.info("创建订单成功, orderNo={}, 消息已写入本地表", order.getOrderNo());
return toResponse(order);
}
}
定时任务 --- 扫描并发送消息
java
@Slf4j
@Component
public class MessageSendScheduler {
private final LocalMessageMapper localMessageMapper;
private final KafkaTemplate<String, String> kafkaTemplate;
@Scheduled(fixedDelay = 5000) // 每 5 秒扫描一次
public void scanAndSendMessages() {
// 1. 查询待发送的消息(status=0 且 retry_count < max_retry)
List<LocalMessage> pendingMessages =
localMessageMapper.selectPending(100);
for (LocalMessage message : pendingMessages) {
try {
// 2. 发送到 Kafka
kafkaTemplate.send(message.getTopic(), message.getMessageId(),
message.getMessageBody()).get(); // 同步等待发送结果
// 3. 发送成功,更新状态
localMessageMapper.updateStatus(message.getId(), 1);
log.info("消息发送成功, messageId={}", message.getMessageId());
} catch (Exception e) {
// 4. 发送失败,增加重试计数
localMessageMapper.incrementRetryCount(message.getId());
log.error("消息发送失败, messageId={}, retryCount={}",
message.getMessageId(), message.getRetryCount() + 1, e);
// 超过最大重试次数,标记为失败(需要人工介入)
if (message.getRetryCount() + 1 >= message.getMaxRetry()) {
localMessageMapper.updateStatus(message.getId(), 2);
log.error("消息重试次数用完, 需要人工处理, messageId={}",
message.getMessageId());
}
}
}
}
}
库存服务 --- 消费消息(幂等处理)
java
@Slf4j
@Component
public class StockDeductConsumer {
private final StockMapper stockMapper;
private final ConsumeLogMapper consumeLogMapper;
@KafkaListener(topics = "stock-deduct-topic")
@Transactional
public void onMessage(ConsumerRecord<String, String> record) {
String messageId = record.key();
log.info("收到库存扣减消息, messageId={}", messageId);
// 1. 幂等检查(防止重复消费)
if (consumeLogMapper.existsByMessageId(messageId)) {
log.info("消息已处理过,跳过, messageId={}", messageId);
return;
}
// 2. 解析消息
StockDeductMessage msg = JsonUtil.fromJson(record.value(), StockDeductMessage.class);
// 3. 扣减库存
int affected = stockMapper.deduct(msg.getProductId(), msg.getQuantity());
if (affected == 0) {
log.error("库存扣减失败(库存不足), productId={}, qty={}",
msg.getProductId(), msg.getQuantity());
// 库存不足时发送补偿消息(取消订单)
sendCompensationMessage(msg.getOrderNo());
return;
}
// 4. 记录消费日志(实现幂等)
ConsumeLog consumeLog = new ConsumeLog();
consumeLog.setMessageId(messageId);
consumeLog.setConsumeTime(LocalDateTime.now());
consumeLogMapper.insert(consumeLog);
log.info("库存扣减成功, productId={}, qty={}", msg.getProductId(), msg.getQuantity());
}
private void sendCompensationMessage(String orderNo) {
// 发送补偿消息,通知订单服务取消订单
// ...
}
}
3.4 优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,易理解 | 有延迟(不是实时一致) |
| 高可用(MQ 解耦) | 需要幂等处理 |
| 对业务侵入小 | 需要本地消息表 + 定时任务 |
| 适合大多数业务场景 | 补偿逻辑需要额外开发 |
四、方案二:TCC(Try-Confirm-Cancel)
4.1 核心思想
每个参与者提供三个接口:
- Try:资源预留(冻结库存、冻结余额)
- Confirm:确认提交(扣减冻结的库存/余额)
- Cancel:取消回滚(释放冻结的资源)
4.2 工作流程
事务协调器
│
├── 阶段1:Try(资源预留)
│ ├── 订单服务.try() → 创建订单(状态:TRYING)
│ ├── 库存服务.try() → 冻结库存(stock_frozen += qty)
│ └── 账户服务.try() → 冻结余额(balance_frozen += amount)
│
│ 全部 Try 成功?
│ ├── 是 → 阶段2:Confirm
│ │ ├── 订单服务.confirm() → 订单状态改为 CONFIRMED
│ │ ├── 库存服务.confirm() → 扣减冻结库存(stock -= qty, stock_frozen -= qty)
│ │ └── 账户服务.confirm() → 扣减冻结余额
│ │
│ └── 否 → 阶段2:Cancel
│ ├── 订单服务.cancel() → 删除订单
│ ├── 库存服务.cancel() → 释放冻结库存(stock_frozen -= qty)
│ └── 账户服务.cancel() → 释放冻结余额
4.3 完整代码示例
库存表设计(增加冻结字段)
sql
CREATE TABLE t_stock (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT NOT NULL,
qty INT NOT NULL DEFAULT 0 COMMENT '可用库存',
frozen_qty INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
UNIQUE KEY uk_product (product_id)
);
-- 冻结记录表(用于 Cancel 时回滚)
CREATE TABLE t_stock_freeze_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
tx_id VARCHAR(64) NOT NULL COMMENT '事务ID',
product_id BIGINT NOT NULL,
freeze_qty INT NOT NULL COMMENT '冻结数量',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0-已冻结 1-已确认 2-已取消',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tx (tx_id)
);
TCC 接口定义
java
/**
* 库存 TCC 服务接口.
*/
public interface StockTccService {
/**
* Try:冻结库存.
*
* @param txId 全局事务ID
* @param productId 商品ID
* @param qty 数量
* @return 是否成功
*/
boolean tryFreeze(String txId, Long productId, Integer qty);
/**
* Confirm:确认扣减(将冻结转为实际扣减).
*
* @param txId 全局事务ID
*/
void confirm(String txId);
/**
* Cancel:释放冻结的库存.
*
* @param txId 全局事务ID
*/
void cancel(String txId);
}
TCC 实现
java
@Slf4j
@Service
public class StockTccServiceImpl implements StockTccService {
private final StockMapper stockMapper;
private final StockFreezeLogMapper freezeLogMapper;
@Override
@Transactional
public boolean tryFreeze(String txId, Long productId, Integer qty) {
log.info("TCC-Try: 冻结库存, txId={}, productId={}, qty={}", txId, productId, qty);
// 1. 幂等检查
StockFreezeLog existLog = freezeLogMapper.selectByTxId(txId);
if (existLog != null) {
log.info("TCC-Try: 已处理过, txId={}", txId);
return existLog.getStatus() == 0;
}
// 2. 扣减可用库存,增加冻结库存(CAS 防止超卖)
int affected = stockMapper.freeze(productId, qty);
// SQL: UPDATE t_stock SET qty = qty - #{qty}, frozen_qty = frozen_qty + #{qty}
// WHERE product_id = #{productId} AND qty >= #{qty}
if (affected == 0) {
log.warn("TCC-Try: 库存不足, productId={}, qty={}", productId, qty);
return false;
}
// 3. 记录冻结日志
StockFreezeLog freezeLog = new StockFreezeLog();
freezeLog.setTxId(txId);
freezeLog.setProductId(productId);
freezeLog.setFreezeQty(qty);
freezeLog.setStatus(0); // 已冻结
freezeLogMapper.insert(freezeLog);
return true;
}
@Override
@Transactional
public void confirm(String txId) {
log.info("TCC-Confirm: 确认扣减, txId={}", txId);
// 1. 查询冻结记录
StockFreezeLog freezeLog = freezeLogMapper.selectByTxId(txId);
if (freezeLog == null || freezeLog.getStatus() != 0) {
log.info("TCC-Confirm: 无需处理, txId={}, status={}",
txId, freezeLog != null ? freezeLog.getStatus() : "null");
return; // 幂等:已确认或已取消
}
// 2. 释放冻结数量(库存已在 Try 阶段扣减了)
stockMapper.releaseFrozen(freezeLog.getProductId(), freezeLog.getFreezeQty());
// SQL: UPDATE t_stock SET frozen_qty = frozen_qty - #{qty}
// WHERE product_id = #{productId}
// 3. 更新状态
freezeLogMapper.updateStatus(txId, 1); // 已确认
}
@Override
@Transactional
public void cancel(String txId) {
log.info("TCC-Cancel: 取消回滚, txId={}", txId);
// 1. 查询冻结记录
StockFreezeLog freezeLog = freezeLogMapper.selectByTxId(txId);
if (freezeLog == null) {
// 空回滚:Try 还没执行就 Cancel 了,直接记录防悬挂
StockFreezeLog emptyLog = new StockFreezeLog();
emptyLog.setTxId(txId);
emptyLog.setProductId(0L);
emptyLog.setFreezeQty(0);
emptyLog.setStatus(2); // 已取消
freezeLogMapper.insert(emptyLog);
return;
}
if (freezeLog.getStatus() != 0) {
return; // 幂等:已确认或已取消
}
// 2. 恢复库存
stockMapper.unfreeze(freezeLog.getProductId(), freezeLog.getFreezeQty());
// SQL: UPDATE t_stock SET qty = qty + #{qty}, frozen_qty = frozen_qty - #{qty}
// WHERE product_id = #{productId}
// 3. 更新状态
freezeLogMapper.updateStatus(txId, 2); // 已取消
}
}
事务协调器(编排 TCC)
java
@Slf4j
@Service
public class OrderTccCoordinator {
private final OrderTccService orderTccService;
private final StockTccService stockTccService;
private final AccountTccService accountTccService;
public OrderResponse createOrder(CreateOrderRequest request) {
String txId = UUID.randomUUID().toString();
log.info("开始分布式事务, txId={}", txId);
boolean orderTryOk = false;
boolean stockTryOk = false;
boolean accountTryOk = false;
try {
// ===== Phase 1: Try =====
orderTryOk = orderTccService.tryCreate(txId, request);
if (!orderTryOk) {
throw new BusinessException("创建订单失败");
}
stockTryOk = stockTccService.tryFreeze(txId, request.getProductId(), request.getQuantity());
if (!stockTryOk) {
throw new BusinessException("库存不足");
}
accountTryOk = accountTccService.tryDeduct(txId, request.getUserId(), request.getTotalAmount());
if (!accountTryOk) {
throw new BusinessException("余额不足");
}
// ===== Phase 2: Confirm(全部 Try 成功) =====
orderTccService.confirm(txId);
stockTccService.confirm(txId);
accountTccService.confirm(txId);
log.info("分布式事务提交成功, txId={}", txId);
return orderTccService.getByTxId(txId);
} catch (Exception e) {
// ===== Phase 2: Cancel(任一 Try 失败) =====
log.error("分布式事务回滚, txId={}, error={}", txId, e.getMessage());
if (orderTryOk) {
orderTccService.cancel(txId);
}
if (stockTryOk) {
stockTccService.cancel(txId);
}
if (accountTryOk) {
accountTccService.cancel(txId);
}
throw e;
}
}
}
4.4 TCC 三大难题
| 问题 | 场景 | 解决方案 |
|---|---|---|
| 空回滚 | Try 未执行,直接收到 Cancel | Cancel 中检查是否有冻结记录,无则直接返回 |
| 幂等 | Confirm/Cancel 被重复调用 | 通过状态字段判断是否已处理 |
| 悬挂 | Cancel 先到,Try 后到 | Cancel 时插入标记记录,Try 检查是否已 Cancel |
4.5 优缺点
| 优点 | 缺点 |
|---|---|
| 强一致性(接近实时) | 实现复杂(每个参与者要写 3 个接口) |
| 不依赖消息队列 | 对业务侵入大(需要拆 Try/Confirm/Cancel) |
| 性能较好(无锁等待) | 需要处理空回滚、幂等、悬挂 |
| 适合短事务 | 数据库需要增加冻结字段 |
五、方案三:Saga 模式
5.1 核心思想
将一个长事务拆成多个本地短事务,每个事务都有对应的补偿操作。如果某一步失败,按逆序执行前面所有步骤的补偿操作。
正向操作:T1 → T2 → T3 → T4
↓ ↓ ↓ ↓
补偿操作:C1 C2 C3 C4
如果 T3 失败:执行 C2 → C1(逆序补偿)
5.2 两种实现模式
编排式(Orchestration)--- 中心化协调
有一个协调器(Orchestrator)负责编排流程:
┌─────────────────┐
│ Saga 协调器 │
│ (Orchestrator) │
└───────┬─────────┘
│
┌───────────────┼───────────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单服务 │ │ 库存服务 │ │ 支付服务 │
└──────────┘ └──────────┘ └──────────┘
协同式(Choreography)--- 事件驱动
无中心协调器,各服务通过事件自行协作:
订单服务 库存服务 支付服务
│ │ │
│── 订单已创建事件 ──→│ │
│ │── 库存已扣减事件 ──→│
│ │ │── 支付已完成事件 ──→
│←── 支付完成通知 ──────────────────────────│
5.3 编排式 Saga 代码示例
Saga 定义
java
@Slf4j
@Service
public class CreateOrderSaga {
private final OrderService orderService;
private final StockService stockService;
private final PaymentService paymentService;
/**
* 执行创建订单 Saga.
*/
public OrderResponse execute(CreateOrderRequest request) {
String sagaId = UUID.randomUUID().toString();
log.info("Saga 开始, sagaId={}", sagaId);
// 定义 Saga 步骤
List<SagaStep> executedSteps = new ArrayList<>();
try {
// 步骤1:创建订单
SagaStep step1 = new SagaStep(
() -> orderService.create(sagaId, request),
() -> orderService.cancel(sagaId)
);
step1.execute();
executedSteps.add(step1);
// 步骤2:扣减库存
SagaStep step2 = new SagaStep(
() -> stockService.deduct(sagaId, request.getProductId(), request.getQuantity()),
() -> stockService.restore(sagaId, request.getProductId(), request.getQuantity())
);
step2.execute();
executedSteps.add(step2);
// 步骤3:扣减余额
SagaStep step3 = new SagaStep(
() -> paymentService.deduct(sagaId, request.getUserId(), request.getTotalAmount()),
() -> paymentService.refund(sagaId, request.getUserId(), request.getTotalAmount())
);
step3.execute();
executedSteps.add(step3);
log.info("Saga 执行成功, sagaId={}", sagaId);
return orderService.getBySagaId(sagaId);
} catch (Exception e) {
// 逆序补偿
log.error("Saga 执行失败, 开始补偿, sagaId={}, error={}", sagaId, e.getMessage());
compensate(executedSteps);
throw new BusinessException("下单失败:" + e.getMessage());
}
}
/**
* 逆序执行补偿操作.
*/
private void compensate(List<SagaStep> executedSteps) {
for (int i = executedSteps.size() - 1; i >= 0; i--) {
try {
executedSteps.get(i).compensate();
} catch (Exception ex) {
// 补偿失败需要记录,后续人工处理
log.error("补偿操作失败, stepIndex={}, error={}", i, ex.getMessage(), ex);
}
}
}
}
SagaStep 抽象
java
/**
* Saga 步骤抽象.
*/
public class SagaStep {
private final Runnable action; // 正向操作
private final Runnable compensation; // 补偿操作
public SagaStep(Runnable action, Runnable compensation) {
this.action = action;
this.compensation = compensation;
}
public void execute() {
action.run();
}
public void compensate() {
compensation.run();
}
}
各服务的正向 + 补偿接口
java
/**
* 库存服务(Saga 参与者).
*/
@Slf4j
@Service
public class StockServiceImpl implements StockService {
private final StockMapper stockMapper;
private final StockOperationLogMapper logMapper;
/**
* 正向操作:扣减库存.
*/
@Override
@Transactional
public void deduct(String sagaId, Long productId, Integer qty) {
log.info("Saga正向-扣减库存, sagaId={}, productId={}, qty={}", sagaId, productId, qty);
// 幂等检查
if (logMapper.existsBySagaId(sagaId)) {
log.info("已处理过, sagaId={}", sagaId);
return;
}
// 扣减库存
int affected = stockMapper.deduct(productId, qty);
if (affected == 0) {
throw new BusinessException("库存不足");
}
// 记录操作日志(用于补偿)
StockOperationLog opLog = new StockOperationLog();
opLog.setSagaId(sagaId);
opLog.setProductId(productId);
opLog.setQty(qty);
opLog.setOperation("DEDUCT");
opLog.setStatus(0); // 0-已执行
logMapper.insert(opLog);
}
/**
* 补偿操作:恢复库存.
*/
@Override
@Transactional
public void restore(String sagaId, Long productId, Integer qty) {
log.info("Saga补偿-恢复库存, sagaId={}, productId={}, qty={}", sagaId, productId, qty);
// 幂等检查
StockOperationLog opLog = logMapper.selectBySagaId(sagaId);
if (opLog == null || opLog.getStatus() == 1) {
log.info("无需补偿, sagaId={}", sagaId);
return; // 没扣过或已补偿
}
// 恢复库存
stockMapper.restore(productId, qty);
// SQL: UPDATE t_stock SET qty = qty + #{qty} WHERE product_id = #{productId}
// 更新日志状态
logMapper.updateStatus(sagaId, 1); // 1-已补偿
}
}
5.4 协同式 Saga 示例(事件驱动)
java
// === 订单服务 ===
@Service
public class OrderServiceImpl {
@Transactional
public void createOrder(CreateOrderRequest request) {
Order order = buildOrder(request);
order.setStatus("PENDING");
orderMapper.insert(order);
// 发布事件,触发下一步
eventPublisher.publish(new OrderCreatedEvent(order.getOrderNo(),
request.getProductId(), request.getQuantity()));
}
// 监听支付成功事件 → 订单完成
@EventListener
public void onPaymentCompleted(PaymentCompletedEvent event) {
orderMapper.updateStatus(event.getOrderNo(), "COMPLETED");
}
// 监听库存扣减失败事件 → 取消订单
@EventListener
public void onStockDeductFailed(StockDeductFailedEvent event) {
orderMapper.updateStatus(event.getOrderNo(), "CANCELLED");
}
}
// === 库存服务 ===
@Component
public class StockEventListener {
// 监听订单创建事件 → 扣减库存
@KafkaListener(topics = "order-created")
public void onOrderCreated(OrderCreatedEvent event) {
try {
stockService.deduct(event.getProductId(), event.getQuantity());
// 成功 → 发布库存扣减成功事件
eventPublisher.publish(new StockDeductedEvent(event.getOrderNo()));
} catch (Exception e) {
// 失败 → 发布库存扣减失败事件(触发订单取消)
eventPublisher.publish(new StockDeductFailedEvent(event.getOrderNo(), e.getMessage()));
}
}
}
// === 支付服务 ===
@Component
public class PaymentEventListener {
// 监听库存扣减成功事件 → 发起支付
@KafkaListener(topics = "stock-deducted")
public void onStockDeducted(StockDeductedEvent event) {
try {
paymentService.pay(event.getOrderNo());
// 成功 → 发布支付完成事件
eventPublisher.publish(new PaymentCompletedEvent(event.getOrderNo()));
} catch (Exception e) {
// 失败 → 发布支付失败事件(触发库存恢复 + 订单取消)
eventPublisher.publish(new PaymentFailedEvent(event.getOrderNo()));
}
}
}
5.5 编排式 vs 协同式对比
| 维度 | 编排式(Orchestration) | 协同式(Choreography) |
|---|---|---|
| 控制方式 | 中心化协调器 | 无中心,事件驱动 |
| 流程可见性 | 高(流程集中在一个类) | 低(分散在各服务的监听器中) |
| 耦合度 | 协调器依赖所有参与者 | 参与者之间通过事件松耦合 |
| 复杂度 | 参与者少时简单 | 参与者多时容易形成事件风暴 |
| 适用场景 | 步骤少(3-5步)、流程清晰 | 参与者多、需要高度解耦 |
| 调试难度 | 容易(看协调器代码) | 困难(需要追踪事件链) |
5.6 优缺点
| 优点 | 缺点 |
|---|---|
| 无全局锁,性能好 | 一致性有延迟 |
| 每步都是本地事务 | 补偿逻辑需要额外开发 |
| 适合长流程 | 不支持隔离性(中间状态可见) |
| 可以跨异构系统 | 调试和问题排查较难 |
六、方案四:Seata 框架
6.1 什么是 Seata?
Seata 是阿里巴巴开源的分布式事务框架,支持 AT、TCC、Saga、XA 四种事务模式,其中 AT 模式对业务侵入最小。
6.2 AT 模式工作原理
┌──────────────────────────────────────────────┐
│ Seata Server (TC) │
│ 事务协调器(全局事务管理) │
└──────────────────────┬───────────────────────┘
│
┌──────────────┼──────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ TM │ │ RM │ │ RM │
│事务发起方│ │资源管理器│ │资源管理器│
│(订单服务)│ │(库存服务)│ │(账户服务)│
└─────────┘ └─────────┘ └─────────┘
| 角色 | 全称 | 职责 |
|---|---|---|
| TC | Transaction Coordinator | 全局事务的协调器(Seata Server) |
| TM | Transaction Manager | 定义全局事务的范围(发起方) |
| RM | Resource Manager | 管理分支事务的资源(参与方) |
AT 模式的神奇之处:只需加一个 @GlobalTransactional 注解,框架自动处理跨服务事务!
原理:
- 执行 SQL 前,记录"修改前的数据"(before image)
- 执行 SQL
- 执行 SQL 后,记录"修改后的数据"(after image)
- 全局事务提交 → 删除 undo log
- 全局事务回滚 → 用 before image 还原数据
6.3 代码示例
依赖
xml
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
配置
yaml
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: nacos
nacos:
server-addr: nacos:8848
namespace: seata
config:
type: nacos
nacos:
server-addr: nacos:8848
namespace: seata
事务发起方(订单服务)
java
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
private final StockFeign stockFeign; // Feign 调用库存服务
private final AccountFeign accountFeign; // Feign 调用账户服务
@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public OrderResponse createOrder(CreateOrderRequest request) {
log.info("全局事务开始, xid={}", RootContext.getXID());
// 1. 创建订单(本地事务)
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setTotalAmount(request.getTotalAmount());
order.setStatus("CREATED");
orderMapper.insert(order);
// 2. 扣减库存(远程调用,Seata 自动传播 XID)
Result<?> stockResult = stockFeign.deduct(request.getProductId(), request.getQuantity());
if (!stockResult.isSuccess()) {
throw new BusinessException("库存扣减失败");
}
// 3. 扣减余额(远程调用)
Result<?> accountResult = accountFeign.deduct(request.getUserId(), request.getTotalAmount());
if (!accountResult.isSuccess()) {
throw new BusinessException("余额扣减失败");
// Seata 会自动回滚步骤 1 和步骤 2
}
// 全部成功,Seata 自动提交全局事务
return toResponse(order);
}
}
事务参与方(库存服务)--- 无需特殊注解
java
@Slf4j
@Service
public class StockServiceImpl implements StockService {
private final StockMapper stockMapper;
@Override
@Transactional // 只需要本地事务注解,Seata 自动管理分支事务
public void deduct(Long productId, Integer qty) {
log.info("扣减库存, xid={}, productId={}, qty={}",
RootContext.getXID(), productId, qty);
int affected = stockMapper.deduct(productId, qty);
if (affected == 0) {
throw new BusinessException("库存不足");
}
}
}
每个参与服务的数据库需要添加 undo_log 表
sql
CREATE TABLE undo_log (
branch_id BIGINT NOT NULL COMMENT '分支事务ID',
xid VARCHAR(128) NOT NULL COMMENT '全局事务ID',
context VARCHAR(128) NOT NULL COMMENT '上下文',
rollback_info LONGBLOB NOT NULL COMMENT '回滚信息',
log_status INT NOT NULL COMMENT '状态',
log_created DATETIME NOT NULL COMMENT '创建时间',
log_modified DATETIME NOT NULL COMMENT '修改时间',
UNIQUE KEY ux_undo_log (xid, branch_id)
) COMMENT 'Seata AT 模式 undo log';
6.4 Seata 四种模式对比
| 模式 | 侵入性 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|---|
| AT | 极低(加注解) | 最终一致 | 中 | 大多数 CRUD 业务 |
| TCC | 高(写3个接口) | 强一致 | 高 | 金融核心 |
| Saga | 中(写补偿) | 最终一致 | 高 | 长流程 |
| XA | 极低 | 强一致 | 低 | 短事务、对性能不敏感 |
6.5 优缺点
| 优点 | 缺点 |
|---|---|
| AT 模式几乎零侵入 | 需要部署 Seata Server |
| 支持多种模式切换 | AT 模式有全局锁,高并发下性能下降 |
| 社区活跃,文档丰富 | 每个库要加 undo_log 表 |
| 与 Spring Cloud 集成好 | 不支持非关系型数据库 |
| 阿里大规模验证 | AT 模式不支持批量 SQL 和复杂 SQL |
七、方案对比总结
| 维度 | 消息队列+补偿 | TCC | Saga | Seata AT |
|---|---|---|---|---|
| 一致性 | 最终一致 | 强一致(接近) | 最终一致 | 最终一致 |
| 实时性 | 有延迟(秒级) | 实时 | 有延迟 | 接近实时 |
| 性能 | 高 | 高 | 高 | 中 |
| 复杂度 | 低 | 高 | 中 | 低 |
| 业务侵入 | 低 | 高 | 中 | 极低 |
| 隔离性 | 无 | 有(冻结) | 无 | 有(全局锁) |
| 适用场景 | 对延迟不敏感的通用业务 | 金融、资金 | 长流程、跨异构 | 通用 CRUD |
| 基础设施 | 消息队列 | 无(自研) | 无/消息队列 | Seata Server |
八、选型建议
你的业务需要强一致性吗?
│
┌──── 是 ──┴── 否 ────┐
↓ ↓
对性能敏感吗? 有延迟容忍度吗?
│ │ │ │
是 ↓ 否 ↓ 是 ↓ 否 ↓
TCC Seata AT 消息队列 Seata AT
+补偿
具体建议
| 场景 | 推荐方案 |
|---|---|
| 电商下单(扣库存+扣款) | Seata AT 或 消息队列+补偿 |
| 银行转账(A 扣 B 加) | TCC |
| 订单→物流→通知(长流程) | Saga |
| 积分发放、优惠券发放 | 消息队列+补偿 |
| 简单 CRUD 微服务 | Seata AT |
| 跨公司/跨系统对接 | 消息队列+补偿 或 Saga |
九、通用最佳实践
9.1 幂等性设计
无论哪种方案,所有参与者都必须支持幂等:
java
// 通过唯一业务ID实现幂等
@Transactional
public void deductStock(String bizId, Long productId, Integer qty) {
// 检查是否已处理
if (operationLogMapper.exists(bizId)) {
return; // 已处理,直接返回
}
// 执行业务
stockMapper.deduct(productId, qty);
// 记录操作(唯一索引防并发)
operationLogMapper.insert(new OperationLog(bizId, "DEDUCT"));
}
9.2 超时处理
java
// 定时任务:处理超时未完成的事务
@Scheduled(fixedDelay = 60000)
public void handleTimeoutTransactions() {
// 查询超过 5 分钟未完成的事务
List<TransactionLog> timeoutList =
txLogMapper.selectTimeout(Duration.ofMinutes(5));
for (TransactionLog tx : timeoutList) {
log.warn("事务超时, 执行补偿, txId={}", tx.getTxId());
compensate(tx);
}
}
9.3 对账机制
生产环境需要定期对账,发现不一致数据:
java
// 每天凌晨对账
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReconciliation() {
// 比对订单表和库存扣减记录
List<String> orderNos = orderMapper.selectTodayOrderNos();
List<String> stockDeductNos = stockLogMapper.selectTodayDeductNos();
// 找出差异
Set<String> missing = new HashSet<>(orderNos);
missing.removeAll(stockDeductNos);
if (!missing.isEmpty()) {
log.error("对账发现不一致, 需要人工处理, orderNos={}", missing);
alertService.sendAlert("对账异常", missing.toString());
}
}
9.4 兜底方案
无论用什么框架,都需要人工介入的兜底:
java
// 补偿失败的记录,发送告警
if (compensateRetryCount >= MAX_RETRY) {
// 写入异常事务表
failedTxMapper.insert(new FailedTransaction(txId, "COMPENSATE_FAILED"));
// 发送告警(钉钉/企微/短信)
alertService.send("分布式事务补偿失败,需要人工处理", txId);
}
十、常见问题 FAQ
Q1: 最简单的方案是什么?
对大多数业务来说:本地消息表 + 消息队列 + 幂等消费。不需要引入额外框架,只需要一张表 + 一个定时任务。
Q2: Seata 适合高并发场景吗?
AT 模式有全局锁,热点数据高并发下会有性能瓶颈。如果 TPS 超过千级且数据有热点(如同一商品秒杀),建议用 TCC 或消息队列方案。
Q3: 如果补偿也失败了怎么办?
- 先重试(有限次数)
- 记录到异常事务表
- 发送告警通知
- 人工介入处理
这是所有分布式事务方案的最终兜底------没有 100% 自动化解决的银弹。
Q4: 如何处理"中间状态可见"问题?
Saga 和消息队列方案中,用户可能看到"订单已创建但库存未扣"的中间状态。解决方式:
- 订单增加"处理中"状态,前端展示"订单处理中,请稍后"
- 使用消息队列确保快速处理(延迟控制在毫秒级)
- 查询时做状态合并(订单+库存+支付状态综合判断)
Q5: 跨数据库的本地事务怎么做?
同一服务连多个数据库时,可以用 Spring 的 JtaTransactionManager + Atomikos:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
但性能较差,尽量避免。更好的做法是按数据库拆分服务边界。
Q6: 消息丢了怎么办?
三重保障:
- 生产端:本地消息表保证一定能发出去(定时重试)
- MQ 端:Kafka/RocketMQ 的 acks=all 保证消息持久化
- 消费端:手动 ACK + 失败重试 + 死信队列
Q7: 哪些场景不需要分布式事务?
- 操作可以重试的(发短信、发邮件)→ 最多多发一次,可接受
- 操作可以异步最终一致的(更新搜索索引、同步数据)→ 消息队列即可
- 单库操作(同一个服务的多个表)→ 本地
@Transactional足够
十一、总结
| 方案 | 一句话总结 | 何时选择 |
|---|---|---|
| 本地消息表 | 本地事务写消息 + 定时发送 + 幂等消费 | 90% 的场景首选 |
| TCC | Try 冻结 → Confirm 确认 → Cancel 释放 | 金融级强一致性 |
| Saga | 正向执行 + 逆序补偿 | 长流程、多步骤 |
| Seata AT | 一个注解搞定,框架自动回滚 | 快速开发、中小规模 |
核心原则:能用最终一致性就不追求强一致性,能用简单方案就不引入复杂框架。 分布式事务的复杂度是实实在在的成本,只在真正需要的地方使用。