分布式事务解决方案:一个老广程序员的血泪实战

架构图画师
2025-12-16 15:18
阅读 666

大家好,我是阿强,一个在广州老西关长大的土著程序员。白天在天河写代码,晚上挤3号线回荔湾——没错,就是那条号称“沙丁鱼罐头专线”的地铁。房贷每月8200,房租省了(多亏祖上留了套60平的老破小),但老婆最近总念叨要不要换学区房,搞得我连奶茶都只敢点中杯。

上周五晚上十点半,我刚从公司溜出来,手机突然疯狂震动。钉钉群里@全体成员:“订单服务和库存服务数据不一致!用户付了钱,库存没扣!”

我心头一紧,手里的肠粉差点掉地上——这可是线上事故。更糟的是,这单子涉及我们新上线的“跨境秒杀”模块,用的是微服务架构,订单、库存、积分、物流全拆开了。我一边狂奔向地铁站,一边心里骂自己:早知道就不贪快用最终一致性方案了!


事情得从去年十月说起

那时候我刚从Java转Go,主要是看中Go在云原生这块的生态。老板说:“阿强啊,你不是老吹Go并发模型牛吗?这次新项目你牵头,用Spring Boot + Go混合架构试试。”

我嘴上答应得爽快,心里其实发虚。毕竟之前十年都在写Java,Spring Boot熟得像自家厨房,但Go只是周末啃过几本电子书。可谁让月薪从15k涨到了22k呢?老婆眼睛一亮:“那学区房首付是不是能凑够了?”

于是项目就这么上了。订单服务用Spring Boot(团队主力语言),库存服务用Go(追求高性能)。两个服务各自有独立数据库,下单流程大概是:

  1. 用户点击“购买”
  2. 订单服务创建待支付订单
  3. 调用库存服务扣减库存
  4. 支付成功后,通知积分服务加积分

理想很丰满,现实很骨感。分布式事务这个老妖精,终于在高并发场景下露出了獠牙。


第一版方案:两阶段提交(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也能做到类似效果——虽然写起来啰嗦点。


血泪教训总结

经过这几个月的折腾,我悟了几个道理:

  1. 不要为了用新技术而用新技术。老板说“要上云原生”,不代表每个模块都得用Go。能解决问题的架构,才是好架构。
  2. 分布式事务没有银弹。2PC、TCC、Saga、消息队列……各有适用场景。在我们这种订单+库存+积分的简单链路里,本地消息表性价比最高。
  3. 幂等性是生命线。无论用什么方案,接口必须幂等。否则重试一次,用户库存多扣100件,客服电话能被打爆。
  4. 监控要到位。我在Prometheus里加了“未处理消息数”指标,超过阈值就告警。再也不用等到用户投诉才发现问题。

写在最后:北漂?不,我是南漂老广

经常有人问我:“广州互联网氛围不行,为啥不北漂?”

我说:我阿妈煲的老火汤,北京给不了。
而且,技术不分南北,只有适不适合

就像分布式事务,硅谷大佬们在推Event Sourcing + CQRS,我们在老城区用本地消息表+肠粉续命,一样能把系统跑稳。

房贷还在还,儿子明年上小学,代码还得继续写。但至少现在,我不再看到“数据不一致”就手抖了。

如果你也在微服务的泥潭里挣扎,记住:先跑起来,再优化。能睡安稳觉的方案,就是好方案

对了,今天地铁上看到招聘广告:某大厂招Go专家,月薪40k起。
我笑了笑,把手机塞回裤兜——
还是回家喝碗艇仔粥实在。


作者:阿强,广州老西关人,白天写Spring Boot,晚上debug Go,梦想是还清房贷后开家糖水铺。

评论 0

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