分布式事务解决方案:我的实战经验分享
引言

作为一名后端架构师,我在多个项目中都碰到过“分布式事务”这个老大难问题。尤其是在近几年微服务架构成为主流的背景下,系统拆得越来越细,但随之而来的是数据一致性保障的难题。今天我想结合自己亲身经历的一个项目,来聊聊我们是怎么解决分布式事务问题的,希望能给同样面临类似挑战的你一些启发。
项目背景

事情要回到2021年我参与的一个电商订单中心重构项目。当时我们的系统已经从原来的单体应用拆分成了多个服务:用户服务、库存服务、支付服务、订单服务和物流服务。每个服务都有自己的数据库,彼此之间通过 RPC 和消息队列通信。
新业务需求来了一个组合下单流程,要求在同一个接口里完成以下几个动作:
- 扣减库存;
- 创建订单;
- 锁定账户余额;
- 推送消息到物流平台准备发货。
这四个操作分别落在不同的服务上,而且任何一个步骤失败都要回滚前面的操作,否则就会出现数据不一致甚至资损的问题。
一开始我们采用了一个看似“简单粗暴”的做法:本地事务 + 最终一致性补偿机制。也就是每步操作完成后都往各自的数据库写个“事务日志”,然后定时任务异步检查是否有未完成事务,并做补偿。刚开始还能应付小规模流量,但随着业务增长,这套方案逐渐暴露出了很多问题。
面临的挑战

1. 补偿逻辑复杂,维护成本高
比如订单创建失败但库存已经扣减了,这时候需要反向调用库存服务进行加库存。但如果补偿请求因为网络问题没收到怎么办?有没有幂等处理?如何保证补偿顺序?这些都要一一考虑,代码变得越来越庞大复杂。
2. 状态一致性难以保障
由于各服务是异步更新的,短时间内很容易出现状态不一致的情况。比如订单状态已经是“已支付”,但库存还没被扣减,导致超卖风险。
3. 调试困难,问题定位慢
补偿流程一旦出错,排查起来非常麻烦。我们需要查订单表、库存表、用户账务表、日志记录、MQ消费情况……整个链路可能涉及十几个服务、几十张表,运维工作量巨大。
4. 性能瓶颈明显
补偿任务频繁地扫描状态、发起远程调用,在高峰时直接把数据库压垮,甚至影响主流程。
这些问题让我们意识到:这种临时性解决方案已经无法满足当前系统的稳定性和扩展性需求。我们必须找一套更合适的分布式事务方案。
解决方案选型与设计思路
我们开始调研各种分布式事务解决方案。最终决定采用 Seata + Saga 模式 作为主方案,部分关键路径采用 TCC 模式兜底。
为什么要选择 Seata 的 Saga 模式?
我们最初也考虑过 XA 模式(两阶段提交),但它对数据库性能损耗较大,尤其在高并发场景下容易卡死。TCC 模式虽然灵活,但需要为每个服务编写正向和补偿方法,实现成本较高。
而 Saga 模式是一个长期运行的事务模型,适用于我们这样多个微服务、长周期操作的业务流程。它的核心思想是:
“如果某一步骤失败,则依次执行之前所有成功步骤的补偿操作。”
也就是说,我们可以在每一个服务中定义好各自的操作和对应的补偿逻辑,由 Saga 引擎负责协调整个流程。
我们选择了 Seata 开源框架,它支持多种事务模式,社区活跃度高,而且已经在不少大厂落地使用。
架构设计概览
整体架构如下图所示:
+------------------+ +------------------+
| Order Service | -----> | Inventory SVC |
+--------+---------+ +--------+---------+
| |
v v
+--------+---------+ +--------+---------+
| Account SVC | <----- | Logistics SVC |
+------------------+ +------------------+
↖ ↗
↘ ↙
↘ ↙
↘ ↙
+--------------+
| Saga Engine |
+--------------+
Saga 引擎作为控制中枢,管理整个事务流程的状态转换。我们为每个子服务提供了两个接口:
- 正向操作(如扣库存)
- 补偿操作(如加库存)
当其中任意一个服务调用失败时,Saga 引擎会按顺序调用之前成功服务的补偿接口,从而保持最终一致性。
数据库设计
为了支持 Saga 模式的事务追踪,我们在每个服务中新增了一张事务状态表,结构大致如下:
CREATE TABLE saga_transaction_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
transaction_id VARCHAR(64) NOT NULL,
service_name VARCHAR(64) NOT NULL,
action_name VARCHAR(64) NOT NULL, -- 如 deductInventory / revertInventory
status ENUM('pending', 'success', 'failed') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
这张表用于记录每次 Saga 执行过程中各个服务的状态,方便故障排查和重试。
关键代码实践
下面是一段简化版的 Saga 事务逻辑,以 Java + Spring Boot 为例:
定义 Saga 流程
public class OrderSaga extends AbstractSaga {
private final InventoryService inventoryService;
private final AccountService accountService;
private final OrderService orderService;

public OrderSaga(...) {
this.inventoryService = inventoryService;
this.accountService = accountService;
this.orderService = orderService;
}
@Override
public void execute() {
try {
// 1. 扣减库存
if (!inventoryService.deductStock(productId)) {
throw new RuntimeException("库存不足");
}
recordAction("deductStock", "inventory");
// 2. 冻结账户余额
if (!accountService.freezeBalance(userId, amount)) {
throw new RuntimeException("冻结余额失败");
}
recordAction("freezeBalance", "account");
// 3. 创建订单
String orderId = orderService.createOrder(...);
recordAction("createOrder", "order");
} catch (Exception e) {
compensate();
log.error("事务失败", e);
}
}
@Override
public void compensate() {
List<SagaStep> executedSteps = getExecutedSteps();
// 倒序执行补偿逻辑
for (int i = executedSteps.size() - 1; i >= 0; i--) {
SagaStep step = executedSteps.get(i);
switch (step.getAction()) {
case "deductStock":
inventoryService.revertStock(step.getProductId());
break;
case "freezeBalance":
accountService.unfreezeBalance(step.getUserId(), step.getAmount());
break;
default:
log.warn("未知的操作: {}", step.getAction());
}
}
}
private void recordAction(String actionName, String serviceName) {
sagaTransactionLogRepository.save(new SagaTransactionLog(UUID.randomUUID().toString(), serviceName, actionName, "pending"));
}
}
这段代码展示了基本的 Saga 流程控制逻辑,包括正常执行和异常情况下的回滚操作。
Saga 引擎配置(伪代码)
# application.yml
saga:
mode: SAGA
retry: true
retry-limit: 3
enable-compensation: true
transaction-manager-host: seata-server-host
我们还集成了 Seata 的 Saga 引擎作为事务编排控制器,通过状态机引擎描述整个事务流程,进一步提升了事务调度的灵活性。
实战踩坑经验
虽然 Saga 是一种相对成熟、稳定的方案,但在实际开发过程中我们也踩了不少坑,总结了几点比较有价值的经验。
1. 补偿操作必须幂等!
我们在上线初期曾遇到一个问题:某个服务因网络原因多次收到补偿请求,结果重复加库存两次,导致数据异常。后来我们增加了唯一事务ID校验机制:
public boolean revertStock(String transactionId, Long productId) {
if (redis.exists("reverted_stock:" + transactionId)) {
return true; // 已补偿
}
// 执行真正的补偿逻辑...
redis.setex("reverted_stock:" + transactionId, 86400, "1"); // 缓存1天
return success;
}
这个措施大大减少了重复补偿带来的副作用。
2. Saga 引擎本身不可靠,需要容灾设计
有一次 Seata Server 挂掉了,导致正在运行中的 Saga 事务全部中断,无法自动恢复。后来我们增加了一个异步监控任务,定期扫描未完成的事务日志,主动触发补偿或重试流程。
@Scheduled(fixedRate = 60_000)
public void checkOrphanTransactions() {
List<String> pendingTxIds = sagaTransactionLogRepository.findPendingTransactions();
for (String txId : pendingTxIds) {
SagaTransactionLog lastStep = sagaTransactionLogRepository.findLastByTxId(txId);
SagaEngine.resumeTransaction(txId, fromStep(lastStep));
}
}
这样即使 Saga 引擎宕机也能尽可能降低数据不一致的风险。
3. 不要把 Saga 当作银弹,适当使用 TCC 作为补充
在一些关键交易路径(如退款)上,我们最终采用了 TCC 模式,因为它可以提供更强的一致性保证,适合金额类场景。
例如:
@Transactional
public boolean prepareRefund(String refundId) {
// 先冻结金额
return accountService.freezeRefundAmount(refundId, amount);
}
public boolean commitRefund(String refundId) {
// 实际扣除冻结资金
return accountService.withdrawFrozenAmount(refundId, amount);
}
public boolean cancelRefund(String refundId) {
// 释放冻结资金
return accountService.releaseFrozenAmount(refundId, amount);
}
这里 TCC 的三段式模型更适合对数据一致性要求高的场景。
4. 日志追踪要完整,不然排查效率低
我们一开始只记录了事务ID和状态,没有埋点完整的操作上下文。后来改造成记录详细参数,并接入 ELK:
{
"tx_id": "abc123",
"service": "inventory",
"action": "deductStock",
"payload": {
"product_id": 1001,
"quantity": 2
},
"status": "success",
"timestamp": "2023-09-15T10:23:00Z"
}
有了这些详细的日志信息,排查起问题来事半功倍。
上线后的效果与收益
我们从 2022 年 Q2 正式将这套 Saga + TCC 的混合方案上线生产环境,经过一年的打磨和优化,收获了不少成果:
- 数据一致性显著提升:跨服务数据不一致的问题减少90%以上;
- 系统稳定性增强:补偿流程自动化程度高,运维压力大大减轻;
- 开发效率提升:Saga 引擎屏蔽了复杂的编排逻辑,业务开发人员只需关注本地事务;
- 容灾能力加强:即使 Saga Server 故障,也有完善的降级机制;
- 性能优化空间变大:异步化之后,核心链路响应速度更快,吞吐量提升约30%。
当然,这也带来了一些额外的开销,比如 Saga 日志存储、补偿重试的资源消耗等,但总体来看收益远大于成本。
给读者的建议与注意事项
如果你也在考虑引入分布式事务,我有几点真心建议想分享:
✅ 尽早统一事务模型
不要等到服务拆多了才去想怎么处理事务一致性问题。越晚介入,改造成本越高。
✅ 根据业务场景灵活选用不同模式
Saga、TCC、XA、SAGA + TCC 混合、甚至本地事务 + 补偿机制都可以共存。关键是根据具体业务特点选择合适的方式。
✅ 注意服务幂等性设计
不管是补偿还是重试,都必须保证调用是幂等的。这是避免雪崩效应的关键防线。
✅ 监控不能少
要有健全的监控体系,包括事务成功率、补偿次数、延迟指标等,及时发现问题并干预。
✅ 技术选型要慎重
像 Seata 这种开源中间件虽好,但也得注意版本兼容性、部署方式、运维复杂度。如果是公司内部自研组件,那更要做好文档、测试和灰度发布。
最后一点感悟:分布式事务是个“妥协的艺术”。永远不可能做到绝对一致性,只能追求最终一致性 + 快速恢复能力。我们要做的,是在稳定性和开发效率之间找到一个合适的平衡点。
结语
这篇文章写到这里差不多快结束了,回头看看这一路走来的酸甜苦辣,真挺感慨的。分布式事务这件事,说简单也简单,说复杂也确实够复杂。但我相信只要我们坚持从真实业务出发,不断迭代优化,就一定能找到最适合自己的解决方案。
希望这篇从一线实战中提炼出来的经验,能够帮助你在面对分布式事务难题时多一份从容,少一些焦虑。如果有什么问题或者你也碰到了类似的坑,欢迎留言交流。
—— 一位热爱折腾的后端架构师 🧑💻

评论 0