分布式事务解决方案:我的一次真实战斗记录
引言:从一个“看似简单的转账”开始

记得去年初,我在一个做金融系统的公司参与核心交易服务的开发。当时,我们正在重构一笔核心业务操作 —— 用户账户之间的资金划转。这个功能乍看之下非常简单:“A 用户转 100 元给 B 用户”,不就是两个数据库操作嘛?减 A 的余额、加 B 的余额?
但随着我们系统逐渐微服务化,这个看似简单的操作就被拆分到了不同的服务中:account-service 负责处理用户的余额变动,而 transfer-service 则作为发起交易的主控方。这个时候问题就来了:
如果 A 减款成功了,但 B 增款失败,那钱丢了怎么办?反之呢?
更头疼的是,我们引入了 Kafka 做消息队列来异步通知下游服务(比如短信通知、风控审计等)。这时候整个事务链就变得更长了。
我第一次真正意识到:这已经不是一个本地事务能解决的问题,而是典型的分布式事务场景。
遇到的挑战:事务边界变模糊、数据一致性难以保障

我们当时遇到的具体问题包括:
- 数据库是 MySQL,跨服务无法使用本地事务;
- 使用了多个服务模块,每个模块都有自己的数据库;
- 消息队列作为中间件,存在投递失败或重复消费的风险;
- 并发操作下,容易出现竞态条件,导致余额计算错误;
- 系统上线初期,出现了几次用户投诉“转账失败但扣款”的情况。
更糟糕的是,由于没有统一的事务协调机制,我们在排查日志时常常一头雾水 —— 不知道哪一个环节出了错,也不知道怎么补偿。
我们尝试的第一种方案:两阶段提交(2PC)
在调研了常见分布式事务方案后,我们最初考虑用的是经典的 2PC(Two-phase Commit)协议。我们选择了 Seata 作为框架支持。Seata 提供了一个比较完整的 TCC 模式和 AT 模式的实现。
我们先试了一下 AT 模式:只要添加注解 @GlobalTransactional,就能把整个方法加入全局事务。听起来很美好,对吧?
但实际部署到测试环境之后,问题就来了:
- 性能下降明显,尤其是在高并发情况下,TPS 下降一半以上;
- 锁冲突严重,MySQL 表级锁被持有时间太久,导致死锁频发;
- Seata 对数据库版本、驱动等要求严格,升级维护成本高;
- 一旦某个服务宕机,整个事务就会处于“不确定状态”,需要人工介入恢复。
更尴尬的是,我们的 Kafka 生产者也想纳入事务控制范围。但 Seata 不支持消息队列的事务性写入,只能手动配合 RocketMQ 的事务机制,复杂度陡增。
最后,我们不得不放弃了 Seata + 2PC 这套方案。
最终采用的方案:TCC + 最终一致性 + 补偿机制
在评估了多种方案之后,我们决定采用相对轻量且灵活的 TCC 模式,并结合“最终一致性”的设计理念进行优化。
什么是 TCC?
TCC 是一种补偿型事务模型,它的核心思想是将原本一个原子事务拆分为三个部分:
- Try(预留资源)
- Confirm(确认执行)
- Cancel(取消操作)
在我们这笔转账业务中,可以这样设计:
- Try:冻结 A 的 100 元;
- Confirm:扣除 A 的 100 元,增加 B 的 100 元;
- Cancel:解除 A 的 100 元冻结。
这种方式的好处在于:
- 事务粒度可控,适合拆分为多个服务调用;
- 无需长时间锁住资源,减少了阻塞;
- 出现失败时可以通过 Cancel 手段进行补偿;
- 可与重试机制结合使用,提升系统容错能力。
实现思路详解
我们采用了自定义 TCC 框架的方式,而不是直接接入成熟组件(比如京东的 jdtcc 或蚂蚁的 tcc-transaction),主要是因为团队熟悉度、学习成本等原因。
整体架构如下:
[ transfer-service ]
|
v
[ account-service - try ] → 冻结金额
|
v
[ account-service - confirm / cancel ]
我们通过本地事务表来记录每一步的状态,并引入了一个定时任务来做兜底补偿。
核心数据库设计(简化版)
CREATE TABLE transfer_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
from_uid BIGINT NOT NULL,
to_uid BIGINT NOT NULL,
amount DECIMAL(18,2) NOT NULL,
status ENUM('try', 'confirm', 'cancel') DEFAULT 'try',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP
);
伪代码示例
public void executeTransfer(Long fromUid, Long toUid, BigDecimal amount) {
// 1. 创建订单,进入 try 状态
TransferOrder order = createOrder(fromUid, toUid, amount);
// 2. Try:冻结 fromUid 的金额
boolean frozen = accountService.freezeAmount(fromUid, amount);
if (!frozen) {
// 冻结失败,直接标记为 cancel 状态
updateOrderStatus(order.getId(), "cancel");
return;
}
// 3. Confirm:向 toUid 加款
boolean transferred = accountService.addBalance(toUid, amount);
if (!transferred) {
// 加款失败,执行 Cancel
accountService.unfreezeAmount(fromUid, amount);
updateOrderStatus(order.getId(), "cancel");
return;
}
// 4. 完成,更新为 confirm
updateOrderStatus(order.getId(), "confirm");
// 5. 发送 Kafka 消息通知下游
kafkaProducer.send(new TransferEvent(fromUid, toUid, amount));
}
当然,这只是主线逻辑。真正的重点是在异常处理和补偿流程上。
踩坑经验分享
这一路上踩了不少坑,下面挑几个最痛的说一说。
1. 幂等性没做好,Kafka 消费重复导致余额出错
刚开始只顾着事务本身,忽略了 Kafka 消费端的幂等性。结果某天压测之后发现用户的账上莫名其妙多出几笔到账记录 —— 消费者重复处理了。
解决方案:给每条消息加上唯一的业务 ID,消费者本地做一个“已处理 ID 缓存”或落盘记录,在处理前先判断是否已处理过。
2. Cancel 失败,资源一直冻结
在某个高峰期,Cancel 接口超时了,用户的钱一直被冻结,投诉电话都打爆了 😅。
解决方案:
- 给 Cancel 操作设置最大重试次数;
- 设置一个“冻结有效期”,超过一定时间自动解冻;
- 使用延迟队列或定时任务来兜底。
3. TCC 服务之间依赖过深,修改困难
TCC 的问题是,你每新增一个参与服务,都需要为其设计 Try/Confirm/Cancel 方法,耦合度很高,后续改起来也很麻烦。
建议:
- 尽量将业务拆小,避免一个 TCC 协议覆盖太多模块;
- 对外提供幂等接口,方便外部系统调用;
- 把通用的 Cancel 操作封装为独立服务或组件。
项目上线后的效果与收益
虽然前期调试花了不少时间,但上线后效果还是很明显的:
| 指标 | 改造前 | 改造后 | 提升幅度 |
|---|---|---|---|
| TPS | ~200 | ~600 | 3x |
| 故障率 | 0.5% | <0.01% | 显著下降 |
| 投诉率 | 平均每天1-2单问题 | 月均0-1单 | 极大降低 |
最重要的是,我们的日志可追踪性和事务完整性大大提升。出现问题也能快速定位,甚至可以通过补偿任务自动修复。
我的一些思考与建议
如果你也在考虑分布式事务的解决方案,这里是我这几年实战下来的一些总结建议:
✅ 选择合适不是最好,要根据业务特点选型
- 本地事务优先,不要一开始就想分布式;
- 如果只是两个服务之间,可以考虑用数据库 XA 事务或消息队列事务;
- 对于长周期、多步骤的操作,推荐使用 TCC 或 Saga 模式;
- 微服务拆得越细,越需要设计好幂等性和回滚机制。
✅ 设计上一定要有“兜底策略”
- 每个关键点都要考虑失败如何恢复;
- 建议保留事务历史记录;
- 写一个补偿任务扫描器,定时检查未完成的事务并尝试修复;
- 结合报警系统,出现异常及时通知。
✅ 一切以用户体验为准绳
技术再牛,最终还是要服务于业务。比如我们曾纠结要不要上 Saga 模式,但它会导致 Cancel 步骤变得复杂,反而影响响应速度。
所以在选型的时候,不妨问自己一个问题:这个问题如果发生在除夕夜,用户会怎么看?
总结一下
这篇文章讲了我在一个真实项目中碰到的一个分布式事务问题,以及我们是如何一步步分析、选型、实践并最终解决问题的过程。
其实到现在我也还在探索更好的方式,比如最近在研究 Event Sourcing 和 CQRS,希望能进一步解耦系统的状态变化。
但无论如何,有一点始终不变:
分布式事务的本质,是对“一致性”与“可用性”权衡的艺术。
希望你在面对类似问题时,能少走弯路,多一些从容 🧠✨
如果你觉得这篇文章对你有用,欢迎点赞、评论或者转发给身边的朋友。也欢迎关注我的公众号【码农日常】,我会继续分享更多接地气的技术实践。

评论 0