分布式事务解决方案:我的一次真实项目实践
引言:为什么我们需要关注分布式事务?

在我做后端开发的这几年里,最让我印象深刻的一个问题,就是“分布式事务”这个老大难问题。尤其是在当前微服务盛行的时代,一个业务流程往往需要多个服务协同完成。每个服务有自己独立的数据库,看似清晰解耦,但一旦涉及到跨服务的数据一致性,各种数据不一致、事务失败的问题就层出不穷了。
今天我想通过一次真实项目的经历,来分享我们在分布式事务方面的解决方案和实践经验。文章会结合背景介绍、遇到的具体挑战、解决思路、实现代码、踩坑经验以及最终效果等几个方面展开,内容尽量做到深入浅出,适合刚接触微服务架构的同学理解掌握。
项目背景:从传统单体系统到微服务转型

我们是一家做电商的公司,系统最初是一个典型的单体架构,所有模块(用户、订单、库存、支付)都在一个项目中运行。随着业务发展和访问量增长,这种结构的弊端逐渐显现:
- 各个功能模块耦合严重,升级部署困难
- 单点故障影响整个系统的可用性
- 数据库连接瓶颈明显,尤其在大促期间经常出现锁等待和超时
于是我们启动了微服务化重构计划,将原本的单体应用拆分成了多个独立服务,比如:
- 用户中心:负责用户的注册、登录、权限管理
- 订单中心:下单、取消、查询订单状态
- 库存中心:商品库存管理与扣减
- 支付中心:处理交易流水和支付回调
拆得是干净利落了,但在实际落地过程中,一个新的问题浮出水面:如何保证跨服务的数据一致性?
特别是订单创建过程,涉及多个服务之间的协调操作:
- 用户下单请求进入订单中心
- 订单中心要调用库存服务确认商品是否有货
- 扣除库存后,生成订单并保存
- 订单创建成功后触发支付,由支付中心处理交易流程
如果这中间任何一个环节失败,都会导致数据不一致,比如已经扣减库存但订单没生成,或者订单生成了但支付失败等等。
这就引出了我们要讨论的核心问题——分布式事务的一致性保障。
面临的挑战:真实的场景问题

挑战一:强一致性 vs 最终一致性
在设计之初,团队对是否要保证强一致性存在分歧:
- 坚持强一致的人认为,必须保证“要么全部成功,要么全部回滚”,否则数据不一致会带来后续难以追责的问题
- 主张最终一致性的则认为,在高并发、多节点的场景下,追求强一致性可能牺牲性能,而且实现起来复杂度太高
这个问题最后还是从业务角度去权衡,得出结论:订单创建这类核心交易操作必须保证强一致性,其他如用户行为统计、优惠券发放等可以接受一定的延迟一致性。
挑战二:技术选型难题
我们先后评估了几种常见的分布式事务解决方案:
两阶段提交(2PC)
- 实现简单,但存在单点故障风险,性能差,不适合高频写入场景
TCC(Try-Confirm-Cancel)模式
- 灵活可扩展,但开发工作量较大,需要为每个操作编写 try、confirm、cancel 三个接口
Saga 模式
- 更加轻量级,适用于长周期流程,但补偿机制复杂且需要幂等性和重试逻辑
基于消息队列的最终一致性方案
- 异步解耦能力强,但无法保证实时一致性,适合非关键路径的数据同步
Seata、Atomikos 等框架集成
- 封装程度高,但学习成本较高,同时对已有系统改造力度较大
经过评估,结合我们当时的技术储备、人力投入以及上线时间窗口,最终决定采用 TCC + 本地事务表 + 补偿任务调度 的方式来应对这一挑战。
解决方案:TCC + 补偿机制的组合拳
我们最终的分布式事务方案是基于 TCC 思路设计的,但为了减少实现成本,没有采用开源框架,而是选择手动实现,并辅以日志记录和后台补偿机制。
核心流程图如下:
[订单服务] [库存服务] [支付服务]
| | |
|----(1) Try---->| |
|<----(OK)-------| |
| | |
|----(2) Try---->| |
|<----(OK)-------| |
| | |
|-(3) Confirm-> | |
|<--(Done)-------| |
| | |
|--------(4) Confirm----------->|
|<--------------(Done)----------|
具体实现步骤如下:
1. Try 阶段(资源预留)
订单中心接收到用户下单请求后,首先开始事务流程:
- 调用库存服务的
decreaseStockTry()接口,进行库存预扣减 - 调用支付服务的
prePay()接口,冻结相应金额 - 如果其中任何一步失败,则直接终止流程,释放已占用资源
2. Confirm 阶段(业务执行)
当所有 Try 成功完成后:
- 调用库存服务的
confirmDecreaseStock()方法,正式扣减库存 - 调用支付服务的
doPay()接口,真正完成支付操作 - 创建订单记录并保存
3. Cancel 阶段(异常回滚)
在任意环节发生异常或超时的情况下:
- 调用库存服务的
cancelDecreaseStock()方法,释放之前预扣的库存 - 调用支付服务的
cancelPrePay()方法,解冻账户资金 - 可选:发送通知给相关人员进行人工干预或自动补偿
4. 日志追踪与补偿机制
我们使用了一个事务状态追踪表来记录每次分布式事务的执行情况:
CREATE TABLE distributed_transaction_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
business_id VARCHAR(64), -- 业务ID,如订单号
service_name VARCHAR(128), -- 当前服务名
phase VARCHAR(32), -- try/confirm/cancel
status ENUM('PENDING', 'SUCCESS', 'FAILED', 'CANCELLED'),
retry_times INT DEFAULT 0,
last_retry_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
每天凌晨跑一次定时任务,扫描所有未完成的事务日志,根据状态进行重试或补偿处理。
关键代码片段展示
下面是一些核心代码片段,展示事务是如何在各服务间流转的。
订单服务:主流程控制
public Order createOrder(String userId, String productId) {
String orderId = generateOrderId();
// Step 1: Try 扣库存
boolean stockReserved = inventoryService.decreaseStockTry(productId, 1);
if (!stockReserved) {
throw new BusinessException("库存不足");
}
// Step 2: Try 冻结支付
boolean payFrozen = paymentService.prePay(userId, totalPrice);
if (!payFrozen) {
inventoryService.cancelDecreaseStock(productId, 1);
throw new BusinessException("支付失败");
}
// Step 3: 确认扣库存
boolean confirmedStock = inventoryService.confirmDecreaseStock(productId, 1);
if (!confirmedStock) {
paymentService.cancelPrePay(userId, totalPrice);
inventoryService.cancelDecreaseStock(productId, 1);
log.error("库存确认失败,进入人工补偿流程");
triggerManualCompensation(orderId);
return null;
}
// Step 4: 完成支付
boolean paid = paymentService.doPay(orderId);
if (!paid) {
log.info("支付失败,准备Cancel流程");
inventoryService.cancelDecreaseStock(productId, 1);
paymentService.cancelPrePay(userId, totalPrice);
return null;
}
// Step 5: 创建订单
Order order = saveOrder(orderId, userId, productId);
return order;
}
库存服务:TCC 接口示例
public class InventoryServiceImpl implements InventoryService {
@Override
public boolean decreaseStockTry(String productId, int quantity) {
// 在Redis中记录预扣库存
Long currentStock = redis.get(productId);
if (currentStock < quantity) {
return false;
}
redis.incrBy(productId, -quantity);
logTransaction(productId, "inventory", "try", "pending");
return true;
}
@Override
public boolean confirmDecreaseStock(String productId, int quantity) {
// 正式从DB中扣除库存
int affectedRows = inventoryDAO.decreaseStock(productId, quantity);
if (affectedRows > 0) {
redis.del(productId); // 删除预扣记录
logTransaction(productId, "inventory", "confirm", "success");
return true;
} else {
log.error("确认扣库存失败");
return false;
}
}
@Override
public boolean cancelDecreaseStock(String productId, int quantity) {
// 返还预扣库存
redis.incrBy(productId, quantity);
logTransaction(productId, "inventory", "cancel", "success");
return true;
}
}
开发中的坑和教训总结
在实际开发过程中,我们也遇到了不少问题,这些经验值得后来人注意:
坑一:幂等性处理不到位导致重复扣减
初期设计时,我们忽视了接口的幂等性处理,导致在网络抖动或重试时,同一个接口被多次调用,造成库存或支付重复扣除。
解决方法:我们在每个接口中都加入了 requestId 参数,用于标识本次请求唯一标识,并在服务端建立请求ID防重机制,使用 Redis 或 MySQL 去重表进行判断。
坑二:补偿机制不够健壮,导致死循环或遗漏
早期我们依赖异步定时任务进行补偿,但由于日志记录格式不统一、状态更新顺序错误等问题,导致部分事务永远处于 “PENDING” 状态,甚至出现无限重试、死循环。
解决方法:
- 增加最大重试次数限制,超过次数转人工介入
- 使用分布式锁防止同一事务被多个任务同时处理
- 统一日志记录格式并引入监控报警机制
坑三:服务之间通信失败导致阻塞主线程
在 Try 和 Confirm 阶段中,我们最初采用了同步 HTTP 请求的方式进行服务调用,结果在高并发下大量线程被阻塞,系统整体性能下降。
解决方法:引入异步+Future模式,结合线程池进行异步编排,提升吞吐能力。
实施后的效果与收益
在生产环境运行了一段时间后,我们的这套分布式事务机制取得了以下成效:
- 订单创建成功率提升了约 12%,主要得益于良好的失败重试机制
- 数据一致性得到了有效保障,关键路径上未再出现明显的不一致问题
- 由于采用了异步补偿机制,高峰期的系统稳定性更高,响应更迅速
运维同学反馈说,通过日志跟踪和后台补偿任务的优化,排查问题的速度快了不少,也减轻了人工干预的压力。
我的经验建议
如果你正在考虑微服务下的事务一致性问题,以下是我总结的一些建议:
✅ 明确业务需求边界
不是所有操作都需要严格的一致性。区分“关键路径”和“非关键路径”,合理选择最终一致性还是强一致性。
✅ 技术方案要因地制宜
不要盲目追求高大上的框架,像 Seata 这样的中间件虽然功能强大,但如果你们团队缺乏相关的维护能力,反而可能成为负担。
✅ 幂等性和重试机制是必须项
无论哪种分布式事务方案,都要考虑网络不确定性带来的重复请求,做好幂等校验,避免“多扣多减”。
✅ 加入可观测能力
包括日志记录、链路追踪、监控报警,是发现问题的关键手段。可以结合 SkyWalking、Zipkin 等工具辅助排查问题。
✅ 善用本地事务表 + 异步补偿
比起复杂的事务协议,一个设计合理的本地事务表配合补偿任务,很多时候能起到事半功倍的效果。
结语:每一步探索都是价值积累
写到这里,我突然想起在项目实施过程中的一些小插曲。
还记得有一次因为 Redis 缓存预扣值和数据库库存不同步,导致一个用户连续下了三次订单才成功。那天晚上我们加班复盘,最后发现问题根源竟然是一个简单的缓存清理时机错误。
那一刻我明白了一个道理:不管多么先进的架构,底层依然是一个个具体的细节堆砌而成的。分布式事务并不神秘,它本质上是对业务流程的精细化控制和对失败场景的周全考虑。
希望这篇文章能帮你在面对类似问题时少走弯路,也希望你能在实战中不断摸索出更适合你团队的解决方案。
如果你有任何疑问或想法,欢迎留言交流~

评论 0