分布式事务解决方案:从踩坑到沉淀的一次实战分享

Redis看门狗
2025-06-28 07:09
阅读 703

开篇:分布式事务,不只是理论名词

开篇:分布式事务,不只是理论名词

我第一次真正面对“分布式事务”这个词的时候,还是三年前在一个支付结算系统的项目里。那时候我们的系统开始接入多个外部金融平台和内部服务模块,数据一致性问题接踵而至,像极了那种你永远也扫不完的“脏数据”——明明上游已经扣款成功,下游库存却没减;或者用户退款到账了,但订单状态却迟迟没有更新。

这些看似“小概率”的问题,在高并发场景下就成了高频故障点,严重时甚至会影响用户体验和资金安全。于是我们不得不正面迎接这个老生常谈却又难以绕过的技术难题:如何在多个独立的服务、数据库中实现事务一致性?

今天我想结合这几年的实际工作经验,尤其是那个让我彻夜难眠的支付结算系统项目,来聊聊我在分布式事务上的一些真实思考和实践心得,希望能给正在这方面迷茫或者踩坑的朋友一点启发。


问题描述:一个“简单”功能引发的连锁反应

缓存策略对比-1

问题描述:一个“简单”功能引发的连锁反应

我们当时要实现的功能是:用户购买课程后,完成支付流程,并同步处理库存、订单、积分等多个模块的操作。起初架构图看起来挺清晰:

  • 订单服务(下单)
  • 支付服务(支付)
  • 库存服务(扣库存)
  • 积分服务(增加积分)

这四个服务各自使用不同的数据库,通过远程调用接口通信。按照最初的设计逻辑,我们采用的是链式的 RPC 调用,每个步骤都串行执行:

订单创建 -> 支付完成 -> 扣库存 -> 加积分

然而现实总是比理想残酷得多:

  • 当某个环节失败怎么办? 比如库存不足或积分服务不可用。
  • 怎么保证前面操作回滚? 例如支付成功后库存扣减失败。
  • 如果服务宕机或者网络超时呢?

这些问题在单体应用时代可以通过本地事务搞定,但在微服务架构下,根本无法靠一个数据库的 ACID 特性来兜底。

更头疼的是,在某个双十一大促期间,这种问题突然爆发,大量订单处于“半死不活”状态,客服电话被打爆,运维值班表上一片红色警报……那段时间,我们几个开发每天都在查日志、写补偿任务、手动修复数据,身心俱疲。


解决方案:不是只有二段提交和 TCC

解决方案:不是只有二段提交和 TCC

针对这类问题,常见的分布式事务解决方案有不少,比如:

  • 两阶段提交(2PC)
  • TCC(Try - Confirm - Cancel)
  • Saga 模式
  • 基于消息队列的最终一致性

但我们在实际选型过程中发现,很多传统方案其实并不适合我们当时的业务场景,原因如下:

方案 优点 缺点 是否适用
2PC 强一致性 性能差,协调者单点故障风险
TCC 性能好,灵活 实现复杂,需编写大量补偿逻辑 部分适用
Saga 易于扩展 逻辑复杂时易出错,补偿依赖顺序
消息+重试机制 异步解耦,性能高 最终一致性,不适合强一致需求 部分适用

我们的最终选择:混合方案 + 状态机驱动

经过多次会议和评估,我们决定采用一种“折中但有效”的方式:

  1. 对于关键路径(比如订单和支付),使用 TCC 模式
  2. 对非核心路径(如积分变动、通知等),采用 消息队列 + 重试机制,并引入本地事务表进行兜底。
  3. 整个流程由一个中心化的状态机引擎来驱动。

这样既保证了关键路径的数据一致性,又避免了过度复杂的架构设计。


技术实现:以状态机为核心驱动业务流程

技术实现:以状态机为核心驱动业务流程

整个流程的核心是一个状态机引擎,它负责控制各个子流程的状态流转。我们选择了 Apache Camel(虽然也可以自己实现),它的 DSL 支持很灵活的状态定义和流程编排。

举个简化版本的流程示意如下:

[Start]
   ↓
CreateOrder(Try) 
   ↓
ChargePayment(Try)
   ↓
DecreaseInventory(Try)
   ↓
AddPoints(Try)
   ↓
CommitAll(Confirm)
   ↓
[End]

如果某一步失败,则触发 Cancel 动作:

[Start]
   ↓
CreateOrder(Try) → OK
   ↓
ChargePayment(Try) → FAIL
   ↓
Rollback: CreateOrder(Cancel)
   ↓
[Error Handling]

当然这只是一个简化版,实际中每一步都有详细的入参校验、幂等性和重试机制。


关键代码片段:以 Java 和 Spring 为例

以下是基于 Spring Boot 和自定义注解的方式,来实现 Try/Confirm/Cancel 的示例框架:

public interface TCCHandler<T> {
    boolean tryAction(T param);
    boolean confirm(T param);
    boolean cancel(T param);
}

订单服务的 Try 接口(伪代码):

@Component
public class OrderTCCHandler implements TCCHandler<OrderParam> {

    @Autowired
    private OrderRepository orderRepo;

    @Override
    public boolean tryAction(OrderParam param) {
        // 创建订单记录,状态为 "pending"
        Order order = new Order(param);
        order.setStatus("pending");
        return orderRepo.save(order) != null;
    }


![数据流转过程-2](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062807/3597e873-7c3b-475a-8bee-c786854f3a24.jpg)


    @Override
    public boolean confirm(OrderParam param) {
        // 更新状态为 "paid"
        return orderRepo.updateStatus(param.getOrderId(), "paid");
    }

    @Override
    public boolean cancel(OrderParam param) {
        // 删除 pending 状态的订单
        return orderRepo.deleteByOrderIdAndStatus(param.getOrderId(), "pending") > 0;
    }
}

支付服务类似,只是会涉及第三方支付通道的回调验证与异步处理。

状态机调度器的大致逻辑如下:

public class TCCStateMachine {

    private List<TCCHandler<?>> handlers; // 注册的 handler 列表

    public boolean execute() {
        for (TCCHandler<?> handler : handlers) {
            if (!handler.tryAction(params)) {
                rollback(handlers);
                return false;
            }
        }

        // 所有 try 成功才进行 confirm
        for (TCCHandler<?> handler : handlers) {
            if (!handler.confirm(params)) {
                log.warn("部分 confirm 失败,后续需人工介入");
            }
        }
        return true;
    }

    private void rollback(List<TCCHandler<?>> executedHandlers) {
        // 倒序取消
        for (int i = executedHandlers.size()-1; i >=0; i--) {
            executedHandlers.get(i).cancel(params);
        }
    }
}

这只是框架级的一个原型参考,真正的线上版本我们会加入:

  • 参数持久化(用于断电恢复)
  • 幂等性处理(防止重复 try/confirm/cancel)
  • 定时任务兜底(自动重试失败动作)

踩坑经验:那些深夜改出来的 Bug 和教训

1. Cancel 未生效?幂等性缺失惹祸!

曾经有一个订单已经 Cancel,但由于网络波动导致 Cancel 请求被重复调用,结果把原本应该保留的订单信息删掉了,最终导致用户投诉。

解决方案:

  • 在 Try 阶段生成唯一 businessId,并作为幂等 key。
  • 每个 Cancel 都带上该 key,在 DB 中加唯一约束字段,避免重复操作。

2. Confirm 之后还继续 Cancel?这是什么逻辑?

状态机逻辑写反了,Cancel 方法被错误地调用到 Confirm 阶段,导致订单异常关闭。

解决方案:

  • 将 Confirm 与 Cancel 方法分离成不同类或加状态标识。
  • 单元测试覆盖所有分支逻辑。

3. 分布式锁用错了反而拖垮系统?

早期我们为了确保同一订单并发调用不会冲突,在 Try 阶段加上了 Redis 锁。结果在高峰期导致大量线程阻塞,QPS 下跌,DB 链接池告急。

优化建议:

  • 锁粒度缩小到具体业务 ID(如 orderId)。
  • 使用 Redis Lua 脚本实现原子性判断与获取锁。
  • 控制超时时间,并配合降级策略。

效果总结:从崩溃边缘走向稳定运行

这套解决方案上线后,带来的收益是实实在在的:

  • 数据一致性问题减少了 90%+
  • 用户投诉率下降 75%
  • 原来的日均 10 条手工补单变成周均 1~2 条
  • 高峰期 QPS 提升了约 30%,响应时间缩短一半

更重要的是,系统整体具备了更强的容灾能力。即使某一个服务出现短暂不可用,也能通过状态机驱动的补偿机制自动修复。


经验分享:给后来者的几点真心话

如果你现在也在面临分布式事务的问题,以下是我总结下来的几条经验:

✅ 1. 不要上来就追求“最强一致性”

在大多数业务中,最终一致性已经足够,除非你做的是银行级别的交易系统。很多时候,“快准稳”比“完全一致”更重要。

✅ 2. TCC 是灵活的,但也容易出错

TCC 的优势在于可控性强、适用于多种业务模型,但它要求你对每一个操作都要明确写出 Confirm 和 Cancel 逻辑。稍有不慎就会导致补偿丢失或错位。

建议: 写一个完整的事务管理中间件模板,封装 Try/Confirm/Cancel 的通用逻辑。

✅ 3. 状态机是个好东西,能让你“看见”整个流程

我们后来将所有的流程都抽象成可视化的状态流转图(用了简单的前端页面 + Camel DSL),每次出问题都能迅速定位到卡在哪个节点,极大提升了排查效率。

✅ 4. 日志和监控不能少

一定要做好日志追踪(比如通过 traceId)和核心指标监控(try 成功率、confirm 数量、rollback 次数等),才能真正掌握系统健康状况。

✅ 5. 补偿机制是最后一道防线

即使做得再完善,也难免出现“灰产”情况。建议定期跑补偿 Job 或使用专门的日志分析工具,自动修复异常流程。


技术趋势展望:未来之路在何方?

如今,随着云原生的发展,越来越多公司开始采用 Seata、Atomikos 这类分布式事务框架,它们降低了开发者理解成本,提供统一的 API 接口。

但我们也要看到,任何框架都存在“黑盒”成分,尤其是在业务复杂、定制化需求高的场景下,自研 + 结合开源组件依然是主流做法。

另外,Event Sourcing(事件溯源)和 CQRS(命令查询职责分离)架构在一些大厂中也开始流行,它们通过“事件流”来驱动整个业务流程,天然适配最终一致性的设计,可能也是未来值得探索的方向。


写在最后:代码背后的人文关怀

分布式事务本质上是技术难题,但它的根源其实是对业务本质的理解和尊重。每一个订单的背后,都是用户的信任;每一笔成功的交易,都承载着产品和服务的价值。

我们做技术的,不只是写代码的人。我们也肩负着保障用户数据不丢、不让一分钱出错的责任。

希望这篇文章对你有所启发。如果你也在分布式事务的路上摸爬滚打过,欢迎留言交流,互相取暖。毕竟这条路,一个人走得慢,一群人可以走得很远。


作者:一个热爱后端架构的码农,经历过无数个通宵调试的夜晚,也见证了一个个小团队逐步走向成熟。愿我们写的每一行代码,都能经得起时间考验。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝