分布式事务解决方案:我在项目中的实战经验分享
背景介绍:为什么我不得不直面分布式事务?

大家好,我是某互联网公司的一名后端开发工程师,主要负责电商平台核心系统的研发。最近一年,我们团队接手了一个重要项目——构建一个高并发、多服务联动的订单履约系统,其中就涉及到了典型的分布式事务场景。
简单来说,这个系统需要完成如下流程:
- 用户下单;
- 订单服务创建订单;
- 库存服务扣减库存;
- 支付服务冻结用户账户余额;
- 仓储服务生成出库单并通知发货。
这些操作分别由不同的微服务处理,每个服务都有自己的独立数据库。一开始,我们尝试使用本地事务来保证各个步骤的原子性。但很快我们就发现,当某个中间步骤失败时,前面的服务已经提交了数据,导致系统进入不一致状态。
比如有一次,订单创建成功、库存扣除完成,但在支付服务调用超时时,我们只能让用户重新支付一次。这种“部分完成”的情况,让我们的客服部门每天要处理大量的异常工单。于是,我们开始思考如何设计一套适用于当前业务场景的分布式事务机制。
面临的挑战:不只是技术问题,更是架构和协作问题

我们面临的几个关键问题包括:
跨服务的数据一致性难保障
每个服务有自己的数据库,不能共享事务,传统的两阶段提交(2PC)又太重,性能扛不住。网络不稳定带来的副作用
某些下游服务响应慢或不可达时,整个流程中断,无法回滚,最终需要人工介入。业务逻辑与事务控制耦合过紧
原本简单的接口调用因为加入了回滚逻辑而变得复杂,维护成本陡增。
当时我们在技术选型上考虑了几种方案,比如TCC(Try-Confirm-Cancel)、Saga模式、基于消息队列的异步补偿,以及Seata这样的开源框架。我们最终根据团队规模、系统复杂度以及运维能力选择了基于TCC模式的补偿事务机制。
解决方案:使用 TCC 实现分布式事务管理

我们决定在订单履约流程中引入TCC模式,其三个阶段分别是:
- Try:资源预留(冻结资金、锁定库存);
- Confirm:执行实际业务(扣除库存、确认支付);
- Cancel:撤销 Try 操作(解冻资金、释放库存);
我们的实现思路如下:
- 将整个流程抽象为一个协调者(Coordinator),由它负责发起各服务的 Try 操作;
- 如果所有 Try 成功,则依次调用各服务 Confirm 接口;
- 如果任何一步 Try 失败或调用超时,则逆序调用 Cancel 接口进行补偿;
- 对于 Cancel 失败的情况,通过定时任务对账并重试。
这里的关键在于每个服务都需要提供完整的 Try/Confirm/Cancel 接口,并确保这三个操作是幂等的,这样即使网络波动也能安全重试。
接口设计示例(以库存服务为例)
// Try 阶段:预扣库存
void holdStock(String orderId, int productId, int quantity);
// Confirm 阶段:正式扣减
void deductStock(String orderId, int productId, int quantity);
// Cancel 阶段:释放库存
void releaseStock(String orderId, int productId, int quantity);
同时,为了追踪每个事务的状态,我们设计了一个全局事务记录表:
CREATE TABLE global_transaction (
id VARCHAR(36) PRIMARY KEY,
business_id VARCHAR(64), -- 如订单ID
status ENUM('TRYING', 'CONFIRMED', 'CANCELED', 'FAILED'),
create_time DATETIME,
update_time DATETIME
);
协调者会在这个表中标记事务状态,并用于后续的补偿机制。
关键代码片段:协调者的执行逻辑

下面是简化版的协调者执行代码(伪代码 + Java 精简实现):
public class TransactionCoordinator {
private List<TccAction> actions = new ArrayList<>(); // 保存所有事务动作
public void execute() throws Exception {
try {
// 第一阶段:Try 所有服务
for (TccAction action : actions) {
boolean success = action.tryIt();
if (!success) {
rollback();
throw new RuntimeException("Try phase failed: " + action.getName());
}
}
// 第二阶段:Confirm 所有服务
for (TccAction action : actions) {
action.confirm();
}
} catch (Exception e) {
rollback();
log.error("Transaction failed, rollback completed.");
throw e;
}
}
private void rollback() {
Collections.reverse(actions); // 逆序回滚
for (TccAction action : actions) {
try {
action.cancel();
} catch (Exception ex) {
log.warn("Cancel failed for action: {}", action.getName(), ex);
// 可记录日志并放入重试队列
}
}
}
}

每个 TccAction 是一个封装好的服务调用组件,负责执行具体的 Try/Confirm/Cancel 方法。
踩坑经验:真实项目中遇到的问题及应对策略
说真的,在实施这套机制的过程中踩了不少坑,下面分享几个印象深刻的教训:
❗ 幂等性没做好,Cancel 接口多次调用引发雪崩
起初我们没有给 Cancel 接口做幂等校验,结果一次系统故障后,多个线程并发执行 Cancel 导致库存被加回多次,最后库存数量远远超出原本值。
解决方法:
- 给每个 Cancel 请求带上唯一 ID;
- 数据库中建立幂等表;
- 同一个 Cancel ID 只能被执行一次。
CREATE TABLE cancel_log (
transaction_id VARCHAR(36),
service_name VARCHAR(64),
request_id VARCHAR(64),
PRIMARY KEY (transaction_id, service_name, request_id)
);
⚠️ 网络超时 + 重试策略不当,造成重复操作
有些服务接口响应较慢,协调者未设置合理的超时时间,导致不断重试同一个 Try 操作,把其他服务压挂。
改进措施:
- 设置最大重试次数限制;
- 使用断路器机制(如 Hystrix)保护后端服务;
- 在协调者中引入异步执行+回调机制,避免阻塞主线程。
🧨 Confirm 或 Cancel 失败后没有自动修复机制
我们最初以为事务执行完就万事大吉了,结果出现网络抖动时,某些 Cancel 操作失败了但没有及时修复,造成脏数据残留。
解决方案:
- 引入定时扫描全局事务状态的任务;
- 对“卡住”状态的事务进行自动补偿;
- 结合报警系统,对长时间处于中间状态的事务主动通知人工干预。
效果总结:稳定性和可维护性大幅提升
上线新机制之后,我们从监控系统中观察到:
- 异常事务数量下降90%以上;
- 客服处理量减少了将近70%;
- 系统整体可用性达到了预期目标;
- 各模块间职责更加清晰,后期扩展也更方便。
而且,我们还积累了一套通用的 TCC 抽象模型,可以迁移到其他类似业务中,比如物流同步、积分变动等场景。
经验分享:写给正在做分布式事务的朋友
如果你也在面对类似的分布式事务问题,以下几点建议或许能帮你少走弯路:
✅ 明确业务边界,合理划分服务范围
分布式事务之所以复杂,往往是因为业务边界不清。尽量将具有强一致性需求的功能集中在一个服务内,减少跨服务交互。
✅ 不要过度依赖分布式事务机制
很多时候可以通过本地事件驱动+异步补偿的方式来规避分布式事务。比如订单支付完成后,通过消息队列异步更新库存,利用幂等消费者和事务性消息保障最终一致性。
✅ 选择适合自己团队的技术方案
TCC、Saga、Seata、RocketMQ 事务消息……每种方案都有适用场景。如果团队人数少且追求可控性,可以选择像我们这样轻量级 TCC 自研;如果追求开箱即用,也可以直接引入 Seata + XA 或 AT 模式。
✅ 做好日志追踪和监控体系
一定要为每个事务分配唯一的 ID,并贯穿所有服务的日志链路。有了全链路跟踪(比如 SkyWalking、Jaeger),排查问题会轻松很多。
写在最后:技术的本质是解决问题,而不是制造复杂
回头来看,分布式事务从来不是一个单纯的技术难题。它考验的是你对业务的理解、对系统边界的把握、对团队能力和运维水平的权衡。
我很庆幸当初没有盲目照搬某种“高大上”的架构方案,而是结合业务场景做了合适的取舍和落地。希望这篇文章能给你一些启发,哪怕只有一点点帮助,那也算我没白折腾这一场。
如果你也在类似项目中有所收获,或者有不同的实践方式,欢迎留言交流!
—— end ——

评论 0