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

Token不够用
2025-06-22 18:38
阅读 1475

开篇背景

开篇背景

我在一家做在线教育的互联网公司做后端开发,主要负责课程报名系统和订单模块。随着业务规模扩大,单体架构已经支撑不了日益增长的请求量,我们决定将服务拆分成多个微服务。这本来是一件好事情,但随之而来的是——分布式事务问题

在一次课程秒杀活动中,用户支付完成之后,系统需要同时更新用户的课程列表、积分余额以及发送通知消息,这些操作分布在不同的服务中。理想情况下,要么全部成功,要么全部失败。但在实际执行过程中,经常会出现部分服务调用失败导致数据不一致的问题。

这个问题严重影响用户体验,也给运维带来了巨大的压力。于是,我开始着手研究并实施一套适合当前项目的分布式事务方案。这篇文章就是我在这段时间里踩过的坑、学到的经验和最终落地方案的总结分享。


问题描述:为什么分布式事务变得如此重要?

问题描述:为什么分布式事务变得如此重要?

当我们的系统还是一个单体应用时,数据库本身支持本地事务,ACID 特性能够保证所有操作原子性地完成。而一旦服务拆分,比如订单服务、账户服务、通知服务各司其职,就不得不面临跨服务、跨数据库的事务协调问题。

具体来说,我们在一次活动订单提交流程中,遇到了如下问题:

  • 用户下单成功后,订单服务写入订单记录;
  • 接着调用账户服务扣除用户余额;
  • 再调用课程服务为用户绑定课程;
  • 最后调用通知服务发送短信或站内信。

这个过程如果出现某一步失败(比如余额扣除成功了,但绑定课程失败),就会导致“脏数据”——用户的钱被扣了但没有获得对应的课程资源。

这种不一致性是不能容忍的,尤其是在涉及到金额的场景下,哪怕只有万分之一的概率,也会造成严重后果。


解决方案选型:如何选择合适的分布式事务方案?

解决方案选型:如何选择合适的分布式事务方案?

当时我们团队内部讨论了很多方案,包括:

  1. 两阶段提交(2PC):理论强一致性,但对性能影响大,且有单点故障风险;
  2. TCC(Try-Confirm-Cancel):补偿机制灵活,但实现成本高,需大量编码逻辑;
  3. Saga 模式:适用于长周期业务,可异步执行,但失败处理复杂;
  4. 本地消息表 + 事务消息(如 RocketMQ):解耦能力强,但依赖消息队列可靠性;
  5. 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;


![缓存策略对比-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062218/8386bd5a-f788-4c22-8f7d-14a6e5914619.jpg)


    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 模式上,用于低频高并发的非关键路径,进一步降低开发成本。


经验建议:给正在做分布式事务的同学几点忠告

  1. 不要一开始就追求完美方案:每种分布式事务方案都有适用场景,先理解你的业务需求再决定。
  2. TCC 虽好,但要写很多补偿逻辑:一定要提前规划好接口定义和状态流转,否则很容易变成一锅粥。
  3. 日志监控不能少:事务状态必须清晰可查,便于后期运维排查问题。
  4. 压测和混沌测试是必要的:在真实环境中,网络问题、服务宕机才是常态。
  5. 保持弹性,未来可迁移:比如现在用了 TCC,将来也可以平滑过渡到 Seata 或者 Event Sourcing 架构。

尾声:写在最后的一些感悟

分布式事务就像是后端开发路上的一道“必答题”。它没有标准答案,也没有万能公式,更多的是结合你自己的业务特点、系统架构和技术栈,找到一种可控、可维护、可扩展的方案。

这次的经历让我深刻体会到,技术从来不只是写代码那么简单,而是要站在更高的视角去权衡和设计。每一个看似不起眼的小功能背后,可能都需要一套完整的机制来保驾护航。

希望这篇分享对你有所帮助。如果你也在折腾分布式事务的问题,不妨留言一起交流,我们一起趟过这片“深水区”。

评论 0

最热最新
暂无评论
Token不够用Lv.1
0
影响力
0
文章
0
粉丝