分布式事务解决方案:一次真实项目中的最佳实践分享
背景介绍:为什么分布式事务如此重要

2021年,我参与了一个电商平台的重构项目,目标是将原本单体架构的服务拆分为多个微服务,以提升系统的扩展性和维护性。我们按业务领域划分了用户中心、订单中心、库存中心、支付中心等子系统,分别部署在不同的服务器上。
一开始一切都挺顺利的,直到我们上线第一个完整业务流程——下单操作的时候,问题就来了。
下单过程包括三个核心动作:
- 扣减库存(调用库存服务)
- 创建订单(调用订单服务)
- 调用第三方支付接口完成支付(调用支付服务)
这三步如果其中任意一个失败,整个流程都应该回滚。但因为是跨服务调用,本地事务已经无法覆盖这些操作了。于是我们遇到了一个典型的分布式事务场景。
这个问题困扰了我们将近两周时间,最后通过引入一套成熟的方案才得以解决。今天我想把这些经验拿出来和大家分享一下,希望能帮助到同样面临这类问题的朋友。
遇到的挑战:不是所有异常都能被 catch 住

最初我们尝试使用最简单的“伪事务”方式来处理,比如:
try {
inventoryService.decreaseStock(productId, quantity);
orderService.createOrder(userId, productId, quantity);
paymentService.charge(amount);
} catch (Exception e) {
// 记录日志并补偿
logger.warn("Order failed, need manual compensation");
}
但实际上,这种方式根本扛不住生产环境的复杂情况。例如:
- 支付成功了,但网络超时导致我们以为失败了,这时候要不要退款?
- 订单创建成功,但下游服务调用失败,如何清理数据?
- 日志写入和调用不同步,出现状态不一致怎么定位?
我们还试过同步重试机制,结果发现一旦某个环节频繁出错,就会导致系统雪崩。甚至有一次,为了快速恢复数据一致性,我们手动写了几十条 SQL 来补数据……
那次之后我们就意识到:不能靠蛮力解决问题,必须找到一种既稳定又可控的分布式事务方案。
我们选择的方案:Seata + Saga 模式实战

我们在技术选型阶段评估了几种主流的分布式事务方案:
| 方案 | 优点 | 缺点 | 使用难度 |
|---|---|---|---|
| 两阶段提交(2PC) | 理论完备 | 性能差,存在单点故障风险 | 较高 |
| TCC(Try-Confirm-Cancel) | 可控性强,适合关键业务 | 开发成本高,需要预留反向逻辑 | 高 |
| Saga 模式 | 流程清晰,支持长周期业务 | 不保证 ACID,需关注补偿失败 | 中 |
| 最大努力通知 | 实现简单 | 几乎不保证一致性 | 低 |
结合我们系统的实际情况(电商下单链路不算特别复杂、允许一定异步补偿、对开发效率有要求),最终决定采用 Seata 的 Saga 模式。
Saga 模式的工作机制简述
Saga 是一种经典的分布式事务模型,核心思想是:每一步都有对应的补偿操作,如果某步失败,则依次执行前面步骤的逆向操作来进行回滚。
举个例子,下单业务可以定义如下状态机:
start -> decreaseInventory(Try) -> createOrder(Try) -> chargePayment(Try)
↑ ↓
└───────rollbackToCreateOrder──┘
每一步执行完成后,会记录当前状态到状态机引擎中。Seata 的 Saga 模式利用 JSON 定义状态流转图,并交由 StateMachineEngine 去驱动整个事务的执行与补偿。
代码实践:从状态图配置到服务整合
第一步:定义状态流转图
我们使用 JSON 描述每个节点的行为及其补偿方法:
{
"name": "orderSaga",
"states": [
{
"name": "decreaseInventory",
"type": "SERVICE_TASK",
"serviceName": "inventoryService",
"serviceMethod": "tryDecreaseInventory",
"compensateState": "cancelInventory"
},
{
"name": "cancelInventory",
"type": "COMPENSATE"
},
{
"name": "createOrder",
"type": "SERVICE_TASK",
"serviceName": "orderService",
"serviceMethod": "tryCreateOrder",
"compensateState": "deleteOrder"
},
{
"name": "deleteOrder",
"type": "COMPENSATE"
},
{
"name": "chargePayment",
"type": "SERVICE_TASK",
"serviceName": "paymentService",
"serviceMethod": "tryCharge",
"compensateState": "refund"
},
{
"name": "refund",
"type": "COMPENSATE"
}
],
"startState": "decreaseInventory",
"endStates": ["success", "failure"]
}

这个文件上传到 Seata Server 以后,就可以通过唯一标识符(比如 stateMachineName)触发执行。
第二步:编写服务接口
以 inventoryService 为例:
@Component
public class InventoryService {
@SagaAction(compensable = true)
public void tryDecreaseInventory(@ContextVariable(name = "productId") Long productId,
@ContextVariable(name = "quantity") Integer quantity) {
// 减库存逻辑,这里是伪代码示例
int updated = inventoryRepository.decreaseStock(productId, quantity);
if (updated == 0) {
throw new RuntimeException("库存不足");
}
}
public void cancelInventory(@ContextVariable(name = "productId") Long productId,
@ContextVariable(name = "quantity") Integer quantity) {
// 补偿动作:加回库存
inventoryRepository.increaseStock(productId, quantity);
}
}
这里需要注意的是:
@SagaAction注解用于标记该方法是 saga 模式中的可执行节点;- 方法入参需加
@ContextVariable注解以便传递上下文; - 各个节点之间的状态和参数都会被 Seata 自动持久化记录。
第三步:触发状态机执行
在下单入口处,调用 Seata 提供的状态机 API 触发执行:
StateMachineEngine engine = SpringUtil.getBean(StateMachineEngine.class);
Map<String, Object> context = new HashMap<>();
context.put("userId", userId);
context.put("productId", productId);
context.put("quantity", quantity);
// 触发执行
StateInstance result = engine.startWithBusinessKey("orderSaga", null, null, context);
踩坑经历:那些深夜里调试出来的经验

说到底,再好的工具也不能避免我们在实际开发中踩坑。以下是几个比较典型的问题及解决方案:
问题一:状态机执行卡住了,不知道进展如何
我们初期上线的时候遇到一个诡异现象:有些订单状态长时间停留在“进行中”,后台看不到任何异常日志。
后来排查发现,Seata 的状态机会把中间状态记录到数据库中,但如果执行过程中抛出未捕获的异常,会导致状态停滞。因此我们做了几点改进:
- 引入全局异常拦截器统一捕获异常并打日志;
- 在 Saga 执行前添加唯一 ID 关联业务单据;
- 在数据库增加定时任务定期清理超时状态机实例。
问题二:补偿函数执行失败,没有自动重试机制
有时候补偿函数也会失败,比如网络抖动导致的支付回滚失败。
我们的应对策略是:
- 对补偿函数也加入重试机制(例如 3 次指数退避);
- 引入独立的消息队列作为容灾通道,当主流程失败时,把订单 ID 投递到 MQ 中进行二次补偿;
- 设置报警机制,一旦超过阈值立即告警,人工介入。
问题三:状态图 JSON 格式错误难排查
Seata 加载状态图的时候并不会严格校验格式,有时候少个逗号或者字段拼写错误,会导致运行时报错,排查起来非常麻烦。
为此,我们做了一个小工具脚本,在 CI 阶段提前校验所有的 .json 文件格式是否符合规范。后来还集成到了 Git Hook 中,大大减少了因格式问题导致的发布事故。
实施效果与收益总结
自从这套 Saga 分布式事务方案正式上线后,整体稳定性得到了显著提升:
- 系统下单流程成功率从原来的 92% 提升到了 99.7%
- 数据不一致率大幅下降,从每天几十笔减少到几乎为零
- 运维同学再也不用半夜爬起来“修数据”了 😂
- 接口响应速度保持在合理范围内(控制在 300ms 内)
此外,这套方案还为我们后续的其他业务模块提供了一个标准模板,比如退货流程、积分兑换等场景都复用了类似的架构,节省了不少开发成本。
给读者的一些建议与注意事项
如果你现在正准备或已经在做分布式事务相关的功能,以下是我个人的一些经验和建议:
不要一开始就追求完美,先解决80%的问题再说
Saga 模式虽然不是强一致性方案,但它足够成熟且具备良好的灵活性。对于大多数业务场景而言,这是性价比最高的选择。始终保留补偿路径的幂等性设计
补偿操作可能多次执行,一定要加幂等判断。比如支付退款接口,必须检查是否已退过款。不要只依赖框架,要有兜底机制
即使用了 Seata,也要结合消息队列、定时补偿任务等方式形成闭环。毕竟线上环境千变万化,多一层保险总没错。日志要详细,追踪要方便
每次 Saga 执行都要带上请求 ID 或订单号,方便定位问题。推荐使用 MDC + 日志平台实现全链路追踪。监控必不可少
监控补偿失败次数、状态机失败率、执行耗时等指标,及时预警,做到心中有数。
结语:技术方案的本质是服务于业务
这篇文章并不是单纯地讲一个技术方案,而是想告诉大家:在真实的工程实践中,技术从来都不是孤立存在的。它必须服务于业务,兼顾运维体验和团队能力。
我们之所以选择 Saga 模式,是因为它在当时最适合我们的业务现状;如果换一个更复杂的场景,也许我们会选择 TCC,或者未来随着 Flink、Event Sourcing 技术的成熟,再升级为基于事件驱动的一致性方案。
最重要的是:不要盲目追新,适合自己才是最好。
希望我的这段经历能够帮到你。如果你在实践中也遇到过类似问题,欢迎留言交流,我们一起成长!
如需获取完整的 Seata Saga 示例代码,可以在评论区留下邮箱,我会整理发送。

评论 0