分布式事务解决方案:从踩坑到落地的一次实战经历
开篇背景

我在一家做在线教育的互联网公司做后端开发,主要负责课程报名系统和订单模块。随着业务规模扩大,单体架构已经支撑不了日益增长的请求量,我们决定将服务拆分成多个微服务。这本来是一件好事情,但随之而来的是——分布式事务问题。
在一次课程秒杀活动中,用户支付完成之后,系统需要同时更新用户的课程列表、积分余额以及发送通知消息,这些操作分布在不同的服务中。理想情况下,要么全部成功,要么全部失败。但在实际执行过程中,经常会出现部分服务调用失败导致数据不一致的问题。
这个问题严重影响用户体验,也给运维带来了巨大的压力。于是,我开始着手研究并实施一套适合当前项目的分布式事务方案。这篇文章就是我在这段时间里踩过的坑、学到的经验和最终落地方案的总结分享。
问题描述:为什么分布式事务变得如此重要?

当我们的系统还是一个单体应用时,数据库本身支持本地事务,ACID 特性能够保证所有操作原子性地完成。而一旦服务拆分,比如订单服务、账户服务、通知服务各司其职,就不得不面临跨服务、跨数据库的事务协调问题。
具体来说,我们在一次活动订单提交流程中,遇到了如下问题:
- 用户下单成功后,订单服务写入订单记录;
- 接着调用账户服务扣除用户余额;
- 再调用课程服务为用户绑定课程;
- 最后调用通知服务发送短信或站内信。
这个过程如果出现某一步失败(比如余额扣除成功了,但绑定课程失败),就会导致“脏数据”——用户的钱被扣了但没有获得对应的课程资源。
这种不一致性是不能容忍的,尤其是在涉及到金额的场景下,哪怕只有万分之一的概率,也会造成严重后果。
解决方案选型:如何选择合适的分布式事务方案?

当时我们团队内部讨论了很多方案,包括:
- 两阶段提交(2PC):理论强一致性,但对性能影响大,且有单点故障风险;
- TCC(Try-Confirm-Cancel):补偿机制灵活,但实现成本高,需大量编码逻辑;
- Saga 模式:适用于长周期业务,可异步执行,但失败处理复杂;
- 本地消息表 + 事务消息(如 RocketMQ):解耦能力强,但依赖消息队列可靠性;
- Seata 框架集成 AT 模式:对业务无侵入,但需引入新组件并注意版本兼容性;
我们团队评估下来,认为当前业务特点更偏向于短生命周期、强一致性要求较高的场景,综合考虑技术栈和人员能力,选择了 TCC + 本地事务结合的方式作为主方案。
TCC 的核心思想是通过 Try 阶段预占资源,然后 Confirm 真正执行操作,或者 Cancel 回退资源。这种方式虽然需要开发者手动编写补偿逻辑,但在我们已有的业务模型中具有较强的可行性。
技术方案与代码实践
接下来我以“用户购买课程”的流程为例,简单展示我们是如何实现 TCC 的。
1. 定义 TCC 接口结构
public interface CourseServiceTccAction {
boolean tryLockCourse(Long userId, Long courseId);
boolean confirmCourseBinding(Long userId, Long courseId);
boolean cancelCourseLocking(Long userId, Long courseId);
}
2. 实现 Try 阶段:锁定课程名额或资源
这一步主要是检查是否有足够库存,并在数据库中标记该用户对该课程的占用状态。
@Override
@Transactional
public boolean tryLockCourse(Long userId, Long courseId) {
// 查询课程库存是否足够
Course course = courseRepository.findById(courseId);
if (course.getStock() <= 0) return false;
// 创建临时锁记录
CourseLock lock = new CourseLock();
lock.setUserId(userId);
lock.setCourseId(courseId);
lock.setStatus("TRYING");
courseLockRepository.save(lock);
// 库存减一(临时)
course.setStock(course.getStock() - 1);
courseRepository.save(course);
return true;
}
3. Confirm 阶段:确认绑定课程
这一步正式将用户加入课程学员表。
@Override
@Transactional
public boolean confirmCourseBinding(Long userId, Long courseId) {
// 更新锁定状态为 CONFIRMED
CourseLock lock = courseLockRepository.findByUserIdAndCourseId(userId, courseId);
lock.setStatus("CONFIRMED");
courseLockRepository.save(lock);
// 添加学员到课程名单
UserCourseRecord record = new UserCourseRecord();
record.setUserId(userId);
record.setCourseId(courseId);
record.setStatus("ACTIVE");
userCourseRepository.save(record);
return true;
}
4. Cancel 阶段:回滚资源释放
如果前面任意步骤失败,Cancel 负责释放 Try 阶段所占资源。
@Override
@Transactional
public boolean cancelCourseLocking(Long userId, Long courseId) {
CourseLock lock = courseLockRepository.findByUserIdAndCourseId(userId, courseId);
if (lock == null || !lock.getStatus().equals("TRYING")) return false;

lock.setStatus("CANCELLED");
courseLockRepository.save(lock);
// 恢复库存
Course course = courseRepository.findById(courseId);
course.setStock(course.getStock() + 1);
courseRepository.save(course);
return true;
}
整个流程由一个全局的事务管理器来控制执行顺序,并根据各个服务返回结果判断是 commit 还是 rollback。
踩坑经验分享
这一套方案听起来很美好,但在真实项目中也踩了不少坑。
坑1:幂等性和重试策略没做好导致重复操作
有一次,confirm 方法因为网络延迟响应超时,事务管理器误判为失败,触发了 Cancel,但其实 confirm 已经完成了部分数据写入。后来发现同一个用户竟然出现了两条学习记录!
解决方式:
- 在每一步加唯一事务ID;
- 使用幂等标识字段,防止重复执行;
- 所有操作都基于状态机判断,避免无效重复。
坑2:Cancel 回滚失败又没兜底机制
某个 Cancel 方法执行失败,事务处于中间状态,既不是成功也不是失败,后续也无法自动恢复。
解决方式:
- 引入定时任务定期扫描未完成事务;
- 对 Cancel 失败的记录进行人工干预;
- 后期引入 Saga 模式+事件溯源来做更完善的回滚补救。
坑3:测试覆盖不全,线上出问题才发现
开发阶段只做了简单的单元测试,缺少压力测试和异常模拟测试。上线初期发生多起数据不一致问题。
解决方式:
- 使用 Chaos Engineering 思想模拟各种异常情况;
- 搭建灰度环境做回归测试;
- 加强日志追踪和链路监控,快速定位问题。
效果总结:这套方案带来的收益
在实施完 TCC 方案后的几个月中,我们监控到了如下改进效果:
- 订单系统的事务一致性错误率下降90%以上;
- 数据不一致的客诉明显减少;
- 系统架构的容错能力和可观测性得到提升;
- 开发团队逐渐熟悉 TCC 和 Saga 的使用模式,能更好地应对复杂的分布式场景。
另外,我们也开始探索将部分流程迁移到 Seata 的 AT 模式上,用于低频高并发的非关键路径,进一步降低开发成本。
经验建议:给正在做分布式事务的同学几点忠告
- 不要一开始就追求完美方案:每种分布式事务方案都有适用场景,先理解你的业务需求再决定。
- TCC 虽好,但要写很多补偿逻辑:一定要提前规划好接口定义和状态流转,否则很容易变成一锅粥。
- 日志监控不能少:事务状态必须清晰可查,便于后期运维排查问题。
- 压测和混沌测试是必要的:在真实环境中,网络问题、服务宕机才是常态。
- 保持弹性,未来可迁移:比如现在用了 TCC,将来也可以平滑过渡到 Seata 或者 Event Sourcing 架构。
尾声:写在最后的一些感悟
分布式事务就像是后端开发路上的一道“必答题”。它没有标准答案,也没有万能公式,更多的是结合你自己的业务特点、系统架构和技术栈,找到一种可控、可维护、可扩展的方案。
这次的经历让我深刻体会到,技术从来不只是写代码那么简单,而是要站在更高的视角去权衡和设计。每一个看似不起眼的小功能背后,可能都需要一套完整的机制来保驾护航。
希望这篇分享对你有所帮助。如果你也在折腾分布式事务的问题,不妨留言一起交流,我们一起趟过这片“深水区”。

评论 0