分布式事务解决方案:一个老广程序员的血泪实战
大家好,我是阿强,一个在广州老西关长大的土著程序员。白天在天河写代码,晚上挤3号线回荔湾——没错,就是那条号称“沙丁鱼罐头专线”的地铁。房贷每月8200,房租省了(多亏祖上留了套60平的老破小),但老婆最近总念叨要不要换学区房,搞得我连奶茶都只敢点中杯。
上周五晚上十点半,我刚从公司溜出来,手机突然疯狂震动。钉钉群里@全体成员:“订单服务和库存服务数据不一致!用户付了钱,库存没扣!”
我心头一紧,手里的肠粉差点掉地上——这可是线上事故。更糟的是,这单子涉及我们新上线的“跨境秒杀”模块,用的是微服务架构,订单、库存、积分、物流全拆开了。我一边狂奔向地铁站,一边心里骂自己:早知道就不贪快用最终一致性方案了!
事情得从去年十月说起
那时候我刚从Java转Go,主要是看中Go在云原生这块的生态。老板说:“阿强啊,你不是老吹Go并发模型牛吗?这次新项目你牵头,用Spring Boot + Go混合架构试试。”
我嘴上答应得爽快,心里其实发虚。毕竟之前十年都在写Java,Spring Boot熟得像自家厨房,但Go只是周末啃过几本电子书。可谁让月薪从15k涨到了22k呢?老婆眼睛一亮:“那学区房首付是不是能凑够了?”
于是项目就这么上了。订单服务用Spring Boot(团队主力语言),库存服务用Go(追求高性能)。两个服务各自有独立数据库,下单流程大概是:
- 用户点击“购买”
- 订单服务创建待支付订单
- 调用库存服务扣减库存
- 支付成功后,通知积分服务加积分
理想很丰满,现实很骨感。分布式事务这个老妖精,终于在高并发场景下露出了獠牙。
第一版方案:两阶段提交(2PC)?别闹了!
一开始我想走正道,上JTA + Atomikos,搞个传统2PC。结果压测一跑,TPS直接从3000掉到200。DBA老李在会议室拍桌子:“你这是要锁死我MySQL主库?明天就滚蛋!”
确实,2PC在跨语言、跨网络的环境下太重了。Go服务那边根本没法无缝接入Java的事务上下文。而且一旦协调者挂了,参与者可能长时间锁住资源——这在线上环境简直是自杀。
那天晚上我在大排档喝着珠江啤酒,跟同事阿豪吐槽:“早知道就全用Spring Cloud Alibaba了,Seata至少能兜底。”
阿豪叼着烟冷笑:“醒醒吧强哥,老板要的是‘技术多元’,不是‘稳妥’。”
转机:本地消息表 + 幂等性
痛定思痛,我翻遍了GitHub和知乎,最后决定回归朴素:本地消息表 + 异步补偿。
具体做法是这样的:
订单服务(Spring Boot):
- 开启本地事务
- 插入订单记录
- 同时插入一条“待发送”消息到
local_message表 - 提交事务
- 后台线程轮询
local_message,调用库存服务(Go)
库存服务(Go):
- 接口设计成幂等的(靠业务ID去重)
- 扣减库存成功,返回200
- 失败则返回特定错误码,订单服务会重试
关键点在于:消息和订单在同一个DB事务里,保证原子性。哪怕库存服务暂时不可用,消息也不会丢,后续重试就行。
我用Go写了个轻量级的消息消费者框架,支持指数退避重试。Spring Boot那边则用@Scheduled轮询,虽然糙,但稳。
上线前夜,我在公司通宵改代码。老婆打来电话:“又加班?明天儿子家长会!”
我一边敲键盘一边哄:“乖啦,爸爸搞完这个bug,周末带你去长隆。”
心里却在想:要是这次再崩,怕是要去送外卖还贷了。
为什么不用Saga或TCC?
有人问:为啥不上Saga模式或者TCC?
说实话,成本太高了。
Saga需要为每个正向操作写对应的补偿逻辑,比如“取消订单”要能“加回库存”。而我们的业务里,“加回库存”涉及复杂的批次、保质期逻辑,写起来比正向还麻烦。
TCC就更夸张了,“Try-Confirm-Cancel”三阶段,每个接口都要改造。Go服务那边光是对接口签名、幂等、状态机就折腾两周,工期根本扛不住。
对我们这种中小团队来说,简单可维护 > 理论完美。本地消息表虽然“土”,但开发快、易理解、故障好排查。上周五那次事故,其实就是消息表没建索引,导致轮询慢了,补个索引就恢复了。
Go 和 Spring Boot 怎么协作?
很多人担心异构语言怎么统一事务语义。我的经验是:别强求统一,只要契约清晰就行。
我们在API层面约定:
- 所有跨服务调用必须带
X-Request-ID - 成功返回
{"code": 200, "data": {...}} - 失败返回
{"code": 4xx/5xx, "message": "xxx"} - 任何操作必须幂等
Go服务里,我用Gin中间件自动记录请求日志,Spring Boot那边用MDC透传TraceID。两边日志一拼,链路就串起来了。
最妙的是,Go的goroutine天然适合做异步任务。库存扣减失败后,我直接开个goroutine后台重试,完全不影响主流程。而Java这边,用CompletableFuture也能做到类似效果——虽然写起来啰嗦点。
血泪教训总结
经过这几个月的折腾,我悟了几个道理:
- 不要为了用新技术而用新技术。老板说“要上云原生”,不代表每个模块都得用Go。能解决问题的架构,才是好架构。
- 分布式事务没有银弹。2PC、TCC、Saga、消息队列……各有适用场景。在我们这种订单+库存+积分的简单链路里,本地消息表性价比最高。
- 幂等性是生命线。无论用什么方案,接口必须幂等。否则重试一次,用户库存多扣100件,客服电话能被打爆。
- 监控要到位。我在Prometheus里加了“未处理消息数”指标,超过阈值就告警。再也不用等到用户投诉才发现问题。
写在最后:北漂?不,我是南漂老广
经常有人问我:“广州互联网氛围不行,为啥不北漂?”
我说:我阿妈煲的老火汤,北京给不了。
而且,技术不分南北,只有适不适合。
就像分布式事务,硅谷大佬们在推Event Sourcing + CQRS,我们在老城区用本地消息表+肠粉续命,一样能把系统跑稳。
房贷还在还,儿子明年上小学,代码还得继续写。但至少现在,我不再看到“数据不一致”就手抖了。
如果你也在微服务的泥潭里挣扎,记住:先跑起来,再优化。能睡安稳觉的方案,就是好方案。
对了,今天地铁上看到招聘广告:某大厂招Go专家,月薪40k起。
我笑了笑,把手机塞回裤兜——
还是回家喝碗艇仔粥实在。
作者:阿强,广州老西关人,白天写Spring Boot,晚上debug Go,梦想是还清房贷后开家糖水铺。

评论 0