分布式事务这道坎,我用两周时间跨过去了
上周五晚上十点,办公室只剩我和运维老张。咖啡机早就凉了,耳机里放着 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