分布式事务解决方案:最佳实践
——一个成都程序员相亲N次后终于脱单的深夜复盘
作者注:写这篇文章时,是2024年3月18日,凌晨1:23。窗外春熙路的霓虹早已熄了大半,我刚哄睡三个月大的女儿,老婆在隔壁房间轻声打着呼噜。桌上泡面桶还没扔,键盘上还沾着昨天加班留下的油渍。但此刻,我很平静。因为我知道,再也不会为“分布式事务”失眠到天亮了——就像我再也不会在相亲软件上滑到凌晨三点。
一、那个让我差点放弃的周五晚上
去年十月的一个周五晚上,9点57分,我瘫在公司工位上,盯着屏幕上不断报错的日志:
[ERROR] OrderService - Failed to deduct inventory: transaction timeout
[WARN] PaymentService - Payment succeeded but order status not updated
用户下了单,钱扣了,但库存没减,订单状态还是“待支付”。这已经是一周内第三次了。
我所在的公司做本地生活服务平台,类似“成都版美团”,技术栈是 Spring Boot + MySQL + Redis。订单、库存、支付三个服务各自独立部署,典型的微服务架构。问题就出在这——当用户下单时,我们需要同时操作三个数据库:创建订单、扣减库存、发起支付。任何一个环节失败,整个流程就得回滚。
但现实哪有这么理想?
那天晚上,运维小李拍了拍我肩膀:“老张,又崩了?你这分布式事务搞不定,月底KPI怕是要凉。”
我说:“哥,我试了三种方案,全翻车了。”
他叹了口气:“要不……改回单体?”
我苦笑。单体?代码都拆成八个服务了,老板画的大饼是“打造西南区最敏捷的微服务中台”,现在说回退?除非我先把自己回滚到单身状态——哦对,那时候我确实还是单身。
二、相亲失败第17次那晚,我决定死磕到底
其实那会儿,我的人生比系统还乱。
月薪15k,在成都租了个一居室,月租3500,水电燃气加起来600。每天下班回家,打开“青藤之恋”,划到眼酸,匹配率不到3%。上周三,第17次相亲对象见面,对方是小学老师,温柔漂亮。聊到一半,她问:“你是做什么的?”
我说:“程序员,搞分布式系统的。”
她眼睛一亮:“哦!是不是像《黑客帝国》那样?”
我尴尬地笑了笑:“更像《西游记》——每个服务都是个猴子,我得把它们全管住,不然就大闹天宫。”
她没懂。饭没吃完就走了。
那天晚上我回到出租屋,泡了碗康师傅红烧牛肉面,坐在电脑前发呆。窗外玉林路的小酒馆传来吉他声,而我的IDEA里,订单服务还在疯狂报错。
“我连自己的感情都搞不定,怎么搞定分布式事务?”
那一刻,我真的想放弃。
但第二天早上,HR突然找我:“老张,有个新机会,薪资能到22k,但要求熟悉高可用架构,特别是分布式事务。”
我心跳漏了一拍。22k!在成都,这意味着我能换个两居室,甚至——敢去高档点的餐厅相亲了。
可问题是:我真能搞定吗?
三、从理论到实战:我在血泪中总结的三种方案
冷静下来后,我决定系统性地梳理分布式事务的常见解法。结合我们用的 Spring Boot 和部分新服务用的 Go(没错,我们开始混搭了),我试了三种主流方案。
方案一:2PC(两阶段提交)——理想很丰满,现实骨感
我先在测试环境搭了个基于 Atomikos 的 JTA 事务管理器,配合 Spring Boot 的 @Transactional 注解。
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderRequest request) {
orderDao.insert(request);
inventoryService.deduct(request.getProductId(), request.getCount()); // 远程调用
paymentService.charge(request.getUserId(), request.getAmount()); // 远程调用
}
理论上,只要所有参与者都“准备”成功,就“提交”;否则全部回滚。
但上线三天后,问题来了:
- 性能极差:每次下单要等所有服务锁表+网络往返,TPS 从 300 直接掉到 40。
- 阻塞严重:有一次库存服务 GC 停顿了 2 秒,整个订单链路卡住,用户疯狂投诉“付了钱没反应”。
- 单点故障:协调者挂了,所有参与者处于“悬而未决”状态,数据不一致。
运维直接骂我:“你这是造了个分布式死锁生成器?”
我默默删掉了配置文件。2PC,适合银行那种强一致性场景,不适合我们这种“宁可少赚,不能让用户崩溃”的互联网业务。
方案二:TCC(Try-Confirm-Cancel)——优雅但太重
不甘心,我又研究 TCC。思路很清晰:
- Try:预留资源(比如冻结库存)
- Confirm:真正执行(扣减冻结库存)
- Cancel:释放预留(解冻)
我在 Go 写的库存服务里实现了这三个接口:
func (s *InventoryService) TryDeduct(ctx context.Context, req *pb.TryDeductRequest) (*pb.TryDeductResponse, error) {
// 检查并冻结库存
if err := s.db.FreezeStock(req.ProductId, req.Count); err != nil {
return nil, err
}
return &pb.TryDeductResponse{Success: true}, nil
}
func (s *InventoryService) ConfirmDeduct(ctx context.Context, req *pb.ConfirmDeductRequest) error {
return s.db.ConfirmFrozenStock(req.ProductId, req.Count)
}
func (s *InventoryService) CancelDeduct(ctx context.Context, req *pb.CancelDeductRequest) error {
return s.db.ReleaseFrozenStock(req.ProductId, req.Count)
}
Spring Boot 的订单服务作为主事务发起方,调用这三个步骤。
优点:最终一致性,性能好,可控性强。
缺点:开发成本爆炸!
每个业务都要写三套逻辑,还要处理幂等、悬挂、空回滚等问题。有一次测试时,Confirm 请求重复发送,库存被扣了两次——用户高兴了,老板差点把我开了。
而且,Go 和 Java 之间的协议对齐也费了老大劲。gRPC 接口定义改了八遍,我和后端同事在会议室吵到保安来敲门。
“这方案太重了,”我对CTO说,“我们团队就5个人,光维护TCC模板就能累死。”
方案三:基于消息队列的最终一致性 —— 我们的“真命天子”
就在山穷水尽时,我想起之前看过的一篇阿里论文:用消息队列实现最终一致性。
核心思想很简单:本地事务 + 异步消息补偿。
具体做法:
- 用户下单时,先在本地事务中创建订单(状态为“待支付”)并发送一条“扣库存”消息到 Kafka;
- 库存服务消费消息,执行扣减;
- 如果扣减失败,消息重试,直到成功;
- 支付服务同理。
关键在于:发消息必须和本地事务原子提交。
我们在 Spring Boot 中这样实现:
@Transactional
public void createOrder(OrderRequest request) {
// 1. 本地创建订单
Order order = new Order();
order.setStatus("pending");
orderDao.insert(order);
// 2. 发送可靠消息(使用事务消息或本地消息表)
messageService.sendMessage("inventory_deduct", buildMessage(order));
}
为了保证消息不丢,我们用了 本地消息表 模式:
- 在订单库中建一张
message_outbox表; - 本地事务同时插入订单和消息记录;
- 后台任务轮询这张表,将消息投递到 Kafka;
- 消费端处理成功后,标记消息为已消费。
这套方案上线后,效果立竿见影:
- TPS 回升到 280+
- 数据不一致率 < 0.01%(基本是网络抖动导致的短暂延迟)
- 开发成本低:Go 服务只需监听 Kafka,无需复杂事务逻辑
更重要的是——它容忍失败。库存服务宕机?没关系,消息堆积,恢复后自动重试。支付超时?订单状态最终会同步。
这不就是生活吗?允许出错,但总有办法兜底。
四、脱单之后,我才真正理解“最终一致性”
有趣的是,就在我搞定分布式事务的那个月,我在朋友婚礼上认识了现在的老婆。她不是程序员,是个幼儿园老师。
第一次约会,我坦白:“我工资不高,在成都也就22k,房子还没买。”
她笑着说:“够啊,玉林路吃顿火锅才80块,幸福又不是靠钱堆的。”
后来她说,喜欢我“做事有始有终”。
我说:“其实我只是学会了接受‘暂时不一致’——就像我们的感情,一开始也不完美,但只要方向对,总会走到一起。”
分布式事务教会我的,从来不是技术本身,而是对不确定性的包容。
2PC 要求绝对同步,像极了年轻时我对爱情的执念——“必须立刻确认关系,否则就是拒绝”。
TCC 太过理想化,每个环节都要精准控制,像我曾经试图“规划好每一步人生”。
而最终一致性告诉我:只要最终能达成一致,过程中的波动和延迟,都是可以接受的。
五、给同行的几点建议
如果你也在为分布式事务头疼,分享几点血泪经验:
- 别迷信强一致性:99% 的互联网业务,最终一致性足够。用户不在乎“实时”,只在乎“结果对不对”。
- 优先用消息队列:Kafka / RocketMQ + 本地消息表,简单、可靠、易维护。我们用这套方案稳定运行半年,0重大事故。
- Go 和 Spring Boot 混搭没问题:只要协议统一(推荐 gRPC 或 JSON over HTTP),语言不是障碍。Go 适合高性能服务(如库存),Java 适合复杂业务编排(如订单)。
- 监控比代码更重要:一定要有消息积压告警、事务状态追踪、补偿任务日志。我们用 Prometheus + Grafana 做可视化,问题秒级发现。
- 人比系统更需要“补偿机制”:累了就休息,错了就重试,别对自己太苛刻。
六、写在最后:代码与生活,都是事务
如今,我的工资涨到了22k,搬进了双楠的两居室,月供4800。女儿出生那天,我在产房外写了最后一段补偿任务的代码——一个定时扫描“异常订单”的脚本。
老婆问我:“你怎么总在关键时刻写代码?”
我说:“因为我知道,只要主事务开始了,就一定能 Commit。”
分布式事务没有银弹,人生也没有标准答案。
但在成都这座慢城里,我学会了:与其追求瞬间的一致,不如相信时间的力量。
愿你的系统稳定,愿你的爱情圆满。
哪怕暂时“不一致”,只要方向对,终会 Commit 成功。
—— 一个终于脱单的成都程序员,于2024年春夜

评论 0