分布式事务这道坎,我用两周时间跨过去了

前端App
2025-12-20 21:28
阅读 1664

上周五晚上十点,办公室只剩我和运维老张。咖啡机早就凉了,耳机里放着 Radiohead 的《Creep》,手指在键盘上敲得发烫——就为了一个破分布式事务的坑。入职新公司才两个月,就被安排重构支付核心链路,压力直接拉满。简历上写“熟悉分布式系统”,结果第一天就发现,自己对“最终一致性”的理解还停留在 PPT 阶段。

说真的,我是个手写代码的老派程序员。虽然最近也在偷偷用 Copilot 辅助写 boilerplate,但核心逻辑?必须一行行敲出来才踏实。这次项目用的是 Java + Spring Cloud Alibaba 架构,数据库是 MySQL 分库分表,订单、账户、库存三个服务各自为政。产品经理一句话:“用户付完钱,库存要减,账户要扣,订单要生成,不能出错。” 好家伙,典型的分布式事务场景。

为什么两阶段提交(2PC)不适合我们?

一开始,我想试试 XA 协议的两阶段提交。毕竟教科书上都这么写。但运维老张冷笑一声:“你是不是忘了我们去年双11因为 2PC 锁表导致整个支付系统雪崩的事?” 一查监控日志,果然——XA 在高并发下性能极差,而且一旦协调者挂了,参与者可能长时间阻塞,资源被锁死。对于我们这种日均百万级订单的系统,根本扛不住。

更别说,Java 的 JTA 实现(比如 Atomikos)配置复杂得像拼乐高,还得引入额外依赖。团队里新人多,谁愿意维护这种黑盒?于是,2PC 被果断否了。

最终一致性才是现实主义者的答案

后来和架构师聊了聊,决定走“最终一致性”路线。具体来说,用 可靠消息 + 本地事务表 的组合拳。思路很简单:每个服务在本地事务中先写业务数据,再往一张 outbox 表里插入一条待发送的消息。然后有个后台任务轮询这张表,把消息发到 RocketMQ(我们用的 Kafka 太重,RocketMQ 轻量又支持事务消息)。

关键在于:写业务表和写 outbox 表必须在一个本地事务里完成。这样即使消息没发出去,也能靠补偿机制重试。

来看个简化版的 Java 代码:

@Transactional
public void createOrder(OrderRequest request) {
    // 1. 创建订单(本地事务)
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setStatus("CREATED");
    orderMapper.insert(order);

    // 2. 插入消息到 outbox 表(同一事务)
    OutboxMessage msg = new OutboxMessage();
    msg.setEventType("ORDER_CREATED");
    msg.setPayload(JsonUtil.toJson(request));
    msg.setProcessed(false);
    outboxMapper.insert(msg);
}

后台任务每秒扫一次 outbox 表,找到未处理的消息,发到 MQ。消费者收到后,执行扣款或减库存操作。如果失败?没关系,消息还在,下次继续重试。

但……万一消息重复了怎么办?

对,这就是幂等性问题。我们所有下游服务都加了幂等校验。比如账户服务,会记录 requestId(由上游生成),如果重复调用,直接返回成功而不重复扣款。

public Response deductBalance(String requestId, DeductRequest req) {
    if (idempotentService.exists(requestId)) {
        return Response.success(); // 已处理过,直接返回
    }
    // 执行扣款逻辑
    balanceService.deduct(req.getAmount());
    idempotentService.markProcessed(requestId); // 标记已处理
    return Response.success();
}

这招虽然土,但有效。上线一个月,没出过资损事故。

区块链?别闹了兄弟

有同事开玩笑说:“要不咱们上区块链?那玩意儿不是天生解决分布式信任问题吗?” 我差点笑出声。区块链的共识机制开销太大,TPS 连我们日常流量的零头都不到。而且,内部系统搞什么去中心化?运维大哥听到这个词就摇头:“又要加节点?又要同步?又要调 Gas 费?滚!”

所以,别被 hype 带偏了。技术选型要看场景,不是看 buzzword。我们的目标是稳定、可维护、能扛住大促,不是发论文。

资源有限,就得精打细算

说到资源,我们团队只有 4 个后端,3 个前端,1 个测试(还兼职产品)。老板画的饼很大,但服务器预算抠得要死。所以方案必须轻量。TCC(Try-Confirm-Cancel)虽然理论上更优雅,但每个接口都要写三套逻辑,开发成本太高。SAGA 模式也类似,状态机复杂,调试困难。

相比之下,本地消息表 + 幂等 这套组合,代码改动小,逻辑清晰,新人三天就能上手。而且不用引入 Seata、DTF 等重型框架,省了运维成本。对我们这种“小步快跑”的创业型团队,再合适不过。

效果如何?数据说话

上线后压测结果如下:

方案 TPS 平均延迟 资源占用 开发周期
2PC (XA) ~300 850ms 高(锁竞争) 2周+
TCC ~1200 320ms 3周+
本地消息表 + 幂等 1800 180ms 1.5周

线上运行两周,99.99% 的事务在 5 秒内完成最终一致。剩下 0.01% 是因为网络抖动,靠重试机制自动恢复。测试妹子都说:“这次居然没让我半夜起来救火。”

一点心得:别迷信“银弹”

写这篇文章的时候,我刚用 Rust 写了个小工具来分析 outbox 表的积压情况。不得不说,Rust 的内存安全和并发模型真香,但现阶段还不适合业务主干。技术栈的选择,永远服务于业务目标

分布式事务没有银弹。2PC 适合强一致但低并发的场景;TCC 适合资金类高敏感业务;而我们这种电商交易,最终一致性足矣。关键是理解业务容忍度——用户能接受几秒的延迟?资损的红线在哪里?

最后,给和我一样刚入职新公司的朋友一句忠告:别急着炫技,先搞懂业务上下文。你的简历上可以写“精通分布式事务”,但生产环境只认结果。搞定了,就是英雄;搞砸了,就得背锅。

哦对了,现在我终于敢在简历上把“分布式事务”从“了解”改成“实战经验”了。虽然过程有点狼狈,但值了。

(耳机里歌换成了《No Surprises》,该下班了。)

评论 0

最热最新
暂无评论
前端AppLv.1
0
影响力
0
文章
0
粉丝