分布式事务解决方案:最佳实践
从一次订单超卖事故说起:我们的分布式事务实战经验

去年冬天,我在参与公司电商平台重构的过程中,遇到了一个让我印象深刻的问题。
系统上线不到两周,促销活动期间出现了商品超卖。当时我们排查日志发现,用户下单、库存扣减和支付处理分别在三个独立服务中完成。这些操作虽然各自完成了本地事务,但整个流程的“一致性”却没有保障。
这个问题像一记闷棍敲醒了我们:微服务架构下,数据一致性不是简单的本地事务能解决的。
那么问题究竟出在哪?
我们的架构图如下(简化版):
[订单中心] <-> [库存中心] <-> [支付中心]
每个中心都有自己的数据库,且通过RPC或者消息队列通信。
问题发生在高峰期的一次促销活动中,大量用户同时发起下单请求。我们采用的是最终一致性的设计理念,也就是先生成订单,再异步通知扣库存,最后确认支付。
但在高并发情况下,异步流程没有保证顺序性,也没有重试机制,导致部分库存没扣减就被其他订单占用——于是出现超卖。
这时候我意识到:我们需要引入分布式事务方案。
常见的几种方案对比与取舍
在讨论具体方案前,我先简单总结一下我们在项目初期尝试过的几个路径:
本地事务 + 人工对账补偿
- 简单粗暴,适合早期业务量小
- 不适用于复杂场景或高频交易
- 对账逻辑复杂,人力成本高
- 最终我们弃用
TCC 模式(Try-Confirm-Cancel)
- 强一致性保障
- 实现复杂,代码侵入性强
- 依赖补偿机制,在网络不稳定时容易出错
- 我们最终只在金融类核心链路使用
Saga 模式
- 更加灵活
- 异常处理较麻烦
- 编排多个服务时状态机设计复杂
- 在一些非强一致场景下使用过
基于消息队列的事务型消费
- 结合本地事务表实现“伪同步”
- 成本低,但需要较强的消息堆积容忍度
- 用于对延迟要求不高的场景
Seata 分布式事务框架
- 统一管理全局事务ID
- 支持AT模式,侵入性较低
- 性能瓶颈随业务规模上升明显
- 中期使用较多,后期做了拆分优化
最终我们选择了一个多模式混合方案:根据业务场景的重要性和一致性需求,选择了不同的事务策略。以下是我亲身经历并落地的一个典型案例。
典型场景实战:积分兑换+虚拟商品发放
项目背景
为了增加用户粘性,我们推出了一套新的积分商城。用户可以使用积分兑换虚拟商品(如视频会员、电子卡券等),兑换过程涉及:
- 用户积分扣减(积分服务)
- 商品库存扣除(商品中心)
- 用户权益记录插入(权益中心)
这三个操作必须保证要么都成功,要么都失败,否则会导致用户的“占便宜”或平台损失。
这里有个小插曲:我们最开始是按“最终一致性”做的,结果刚上线就收到客服反馈,“用户扣了积分,没拿到卡密”,查下来就是因为异步流程丢失了某条消息。
技术挑战
这个场景有几个痛点:
- 三个服务属于不同团队维护,接口调用频繁变更,统一框架接入成本大;
- 要求响应时间控制在 500ms 内,不能让用户等待太久;
- 涉及到数据库的幂等校验、事务回滚、补偿执行;
- 幂等性设计、消息去重、异常恢复机制都需要考虑周全。
解决思路
我们最终采用了 TCC + Saga 的混合模式。结合了 TCC 的强一致性优势和 Saga 的灵活性。
架构简述如下:
[积分商城网关] -->
[积分服务 Try 接口]
[商品服务 Lock 接口]
[权益服务 Record Prepare]
|
|---- 如果全部成功 --> Confirm 接口
|
|---- 如果任一步失败 --> Cancel 接口
关键点设计
Try 阶段(资源预占)
- 积分服务冻结积分
- 商品服务预留商品库存
- 权益服务插入占位记录(未生效)
Confirm 阶段(资源提交)
- 积分实际扣除
- 库存真正减少
- 权益记录变为已生效状态
Cancel 阶段(资源释放)
- 积分解冻
- 商品释放锁定库存
- 权益记录标记为作废
异常处理机制
- 所有接口需支持幂等
- 网关层加入唯一请求 ID
- 日志记录+定时任务检查未完成事务
- 使用 Redis 缓存事务状态,避免 DB 查询压力过大
数据库设计优化
考虑到性能与一致性兼顾,我们在设计数据库时做了几项关键调整:
- 使用乐观锁控制库存修改,避免并发冲突
- 所有事务操作使用 UUID 作为幂等标识
- 权益表设计为 soft delete 模式,方便后续审计和回滚
接口设计规范
为了避免服务间互相耦合太深,我们制定了严格的接口规范:
// Try 接口示例
public class TryResult {
private String transactionId;
private boolean success;
private String resourceId; // 如商品ID
}
// Confirm 接口示例
public void confirm(String transactionId, String resourceId);
// Cancel 接口示例
public void cancel(String transactionId, String resourceId);
所有调用方都遵循统一的返回格式,并由中间件做统一的兜底处理。
实施效果与收益
这个方案上线后,我们监控到以下几个变化:
| 指标 | 上线前 | 上线后 | 变化幅度 |
|---|---|---|---|
| 积分兑换失败率 | 0.8% | 0.02% | ↓97.5% |
| 客服投诉数 | 每天约3起 | 几乎为零 | ↓近100% |
| 接口平均响应时间 | 320ms | 410ms | ↑90ms |
| 系统异常自动恢复能力 | 无 | 自动触发Cancel | 显著提升 |

虽然响应时间有所增加,但控制在可接受范围内。而且系统健壮性提升非常显著,运营同学也少了很多对接工作。
我们的运维经验和几点建议
这套分布式事务体系上线后,我们也积累了一些宝贵的生产运维经验:
1. 别盲目追求强一致性
很多开发同学看到“分布式事务”就想着必须上 Seata 或者 XA 框架,但实际上并不是所有场景都需要强一致性。
比如普通日志落库、异步推送这类非关键路径,完全可以走异步+补偿机制,这样既能节省性能开销,也能降低系统复杂度。
2. 幂等设计一定要前置考虑
在高并发场景下,重复请求是常态。如果不做好幂等处理,很容易造成资金/数据错误。
我们后来统一规定:所有对外接口必须携带 requestId,后台用 redis 缓存请求记录,防止重复执行。
3. 不要忽略事务日志
事务状态的变化一定要完整记录,便于后续对账和排查问题。
我们设计了一个轻量级的事务日志模块,记录每笔事务的生命周期,包括:
- 当前阶段(try、confirm、cancel)
- 各子步骤执行结果
- 错误信息(如有)
- 时间戳
有了这份日志,定位问题效率提升了80%以上。
4. 合理拆分业务边界
刚开始我们把一个大事务包含五个服务的操作,后来发现协调复杂、性能差。后来我们逐步进行了服务解耦,将事务边界划分得更清晰。
举个例子:
- 订单创建和支付划分为两个独立事务
- 会员权益更新走异步补偿
- 核心积分扣减保留强一致性
这种拆解方式让系统更容易扩展和维护。
我的一些思考和总结
这次分布式事务的实践给我带来的最大启示是:分布式事务的本质不是技术问题,而是业务设计问题。
如果你的业务边界划分不清楚,即使引入再多的框架也难以根治问题。
在日常开发中,我推荐大家注意以下几点:
事务边界越早确定越好
- 从产品设计阶段就开始考虑一致性需求
- 开发前明确哪些操作必须绑定在一个事务中
选型要因地制宜
- 简单场景可用“本地事务+对账补偿”
- 核心路径可用TCC
- 非关键路径可以考虑Saga或消息最终一致
监控与报警是系统的生命线
- 加入事务状态看板
- 对异常事务实时预警
- 提供一键修复工具
保持系统可演进
- 不要一次性设计得太重
- 随着业务发展逐步完善事务机制
写在最后
这篇文章其实也是我过去一年踩坑、修复、总结的过程记录。
也许你正在面临类似的分布式事务难题,也可能正准备实施某个一致性方案。希望我分享的经验能帮你在权衡利弊、做出决策时,多一份从容和底气。
当然,技术没有银弹,只有不断迭代和优化。愿你我在复杂系统的世界中,都能走出一条清晰且稳健的路。

评论 0