请写一篇关于【分布式事务解决方案:最佳实践】的技术文章
作者:一个刚拿大厂offer、在杭州背30年房贷的应届生
去年十月,我还在出租屋里啃着泡面刷LeetCode,房租3500,存款不到2万,女朋友(现在是老婆了)问我:“你确定要投这个岗位?听说他们分布式系统特别难搞。”
我说:“不就是事务嘛,ACID我都背得滚瓜烂熟。”
她白了我一眼:“你背的是数据库课考试题,人家问的是‘订单-库存-积分’三服务怎么保证一致性。你连微服务都没跑起来过。”
我当时心里咯噔一下——原来面试题和现实之间,差了整整一个生产环境的距离。
一、入职第一天,我就被“分布式事务”打脸了
今年三月,我终于拿到杭州某一线大厂的后端开发offer,月薪从实习期的15k涨到22k(税前!)。签完合同那天,我和老婆在小区门口的小馆子点了两碗片儿川,加了个卤蛋庆祝。她说:“这下房贷有希望了。”我们买的是未来科技城边上68平的小两居,月供6800,压力不小,但至少不用再看房东脸色。
入职第一周,组长老张(花名“阿哲”,工牌号比我还小两位数)把我拉进一个叫 order-service-v3 的项目群。群里消息刷得飞快:
“订单创建失败,库存扣了,积分没加,用户投诉了!”
“TCC回滚没触发,查日志!”
“谁动了Saga的状态机配置?!”
我一脸懵,偷偷问旁边工位的学长:“这些不都是事务问题吗?为啥不用数据库事务解决?”
他苦笑:“兄弟,你当这是单体应用啊?我们现在光订单服务就部署在三个AZ(可用区),库存服务是Go写的,积分服务是Java,用户中心还是Node.js……你让MySQL怎么跨服务回滚?”
那一刻,我意识到:课本里的“BEGIN; UPDATE; COMMIT;”在真实世界里,根本不够用。
二、面试题 vs 真实场景:我踩过的三个大坑
坑1:以为“最终一致性”等于“随便丢数据”
面试时被问:“你们怎么处理分布式事务?”
我自信答:“用消息队列做最终一致性,比如RocketMQ事务消息。”
HR点头:“不错,有思路。”
结果上班第三天,我就把测试环境搞崩了。
当时我负责一个优惠券核销功能:用户下单 → 扣减库存 → 发放积分 → 核销优惠券。我用Kafka发了一条“订单完成”消息,下游监听后执行积分和优惠券逻辑。
问题来了:如果积分服务挂了,消息丢了怎么办?
我没做重试机制,也没做幂等。结果测试同学反复下单,积分账户直接多了5000分,运营差点报警。
老张把我叫过去,语气平静但眼神犀利:“你知道公司每天订单量多少吗?2000万单。你这一丢,不是几毛钱的事,是合规风险。”
我当晚加班到凌晨两点,补上了:
- 消息ACK机制
- 消费失败自动重试(指数退避)
- 积分接口加唯一请求ID防重
教训:最终一致性 ≠ 放任不管,而是“可控地延迟一致”。
坑2:盲目上TCC,结果把自己绕晕了
后来项目要支持“预占库存+支付超时释放”,技术方案讨论会上,架构师说:“上TCC吧,Try-Confirm-Cancel,业界标准。”
我一听,热血沸腾,立马GitHub搜了一堆Go实现,比如 dtm、seata-go,兴冲冲搭了个Demo。
结果Try阶段预占库存成功,Confirm阶段支付回调超时,Cancel却因为网络抖动没执行——库存被锁死,用户无法下单。
更惨的是,TCC要求每个服务都实现三个接口,我改了订单、库存、优惠券三个服务,代码重复率高得离谱。老婆看我半夜还在画状态转换图,心疼地说:“你是不是选错行了?要不考公务员吧?”
我苦笑:“公务员也得处理事务啊,只是他们叫‘流程审批’。”
最后我们妥协了:核心链路用Saga模式 + 补偿事务,非关键操作走异步消息。
毕竟,不是所有场景都需要强一致性。有时候,“能退钱”比“不能出错”更用户友好。
坑3:迷信“区块链能解决一切信任问题”
有次团建,隔壁组一个搞区块链的哥们喝高了,拍桌子说:“你们这分布式事务太原始了!上区块链啊!智能合约自动执行,天然不可篡改!”
我当时还真信了,回去查了一堆资料,甚至fork了一个Hyperledger Fabric的Go SDK仓库。
结果发现:TPS不到100,部署复杂度爆炸,运维成本高到能买下我那套房子的首付。
而且,区块链解决的是“多方互不信任”的场景,而我们内部服务之间,明明是“高度信任”的——大家都是同一个老板发工资!
老张知道后笑得直拍桌子:“你是不是看了太多币圈新闻?咱们又不是要做去中心化电商。”
醒悟:技术选型要看业务本质,不是追热点。
三、我的“土法炼钢”最佳实践(附Go代码片段)
踩了这么多坑,我总结出一套适合中小团队的分布式事务实践,不求高大上,只求稳:
1. 能合并服务,就别拆
先问自己:这个功能真的需要独立部署吗?如果QPS不到1000,别硬拆微服务。单体应用+本地事务,永远是最香的。
2. 异步解耦 + 幂等 + 重试
对于非核心链路(如发通知、记日志),用消息队列。但必须做到:
- 消息体带唯一ID(如
request_id: order_123456) - 消费端做幂等(Redis setnx 或 DB唯一索引)
- 失败自动重试,最多3次,之后进死信队人工处理
// Go示例:幂等积分发放
func AddPoints(ctx context.Context, req *AddPointsRequest) error {
key := fmt.Sprintf("points_req:%s", req.RequestID)
if redisClient.SetNX(ctx, key, "1", 24*time.Hour).Val() {
return errors.New("duplicate request")
}
// 执行积分逻辑...
return nil
}
3. 核心链路用Saga + 补偿
对于“下单-扣库存-扣余额”这种关键路径,用Saga模式。每个步骤成功后记录状态,失败则逆向补偿。
我们在Go里用状态机实现:
type OrderSaga struct {
Status string // "created", "stock_locked", "paid", "failed"
}
func (s *OrderSaga) Execute(ctx context.Context) error {
switch s.Status {
case "created":
if err := lockStock(); err != nil {
s.compensateCreated()
return err
}
s.Status = "stock_locked"
case "stock_locked":
if err := chargeUser(); err != nil {
s.compensateStockLocked()
return err
}
s.Status = "paid"
}
return nil
}
重点:补偿操作本身也要幂等、可重试!
4. 监控 & 对账是最后防线
无论多完美的方案,都要有兜底:
- 每小时跑对账脚本,比对订单、库存、积分数据
- 关键事务埋点,Grafana看板监控成功率
- 报警阈值设为99.9%,低于就call人
四、写在最后:技术人的成长,从来不是一蹴而就
上周五晚上,我又加班到十点。回家路上,老婆发微信:“今天还房贷了吗?”
我说:“转了,顺便看了眼GitHub,star了那个dtm项目。”
她回了个捂脸笑的表情:“你呀,还是那个为一行代码较真的傻子。”
其实我知道,从学生到工程师,最大的转变不是技术栈,而是责任意识。以前写bug只是作业挂科,现在写bug可能让用户损失真金白银。
分布式事务没有银弹,只有权衡。就像我在杭州买房,明知月供压得喘不过气,但依然选择扎根——因为相信长期价值。
如果你也在准备面试,别只背“Seata原理”“XA协议流程”。去GitHub跑一遍开源项目,自己搭个Go微服务,故意制造几个事务异常。那种深夜debug后突然通电的感觉,比任何面经都珍贵。
共勉。
作者注:本文所有案例均来自真实项目(已脱敏)。代码仅为示意,生产环境请结合业务谨慎使用。
我的GitHub主页:@hangzhou-dev(匿了,怕被认出来)
下期预告:《在大厂写CRUD,是一种什么体验?》

评论 0