找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1865

积分

0

好友

246

主题
发表于 2025-12-31 09:58:37 | 查看: 20| 回复: 0

两个月前,我分享了《从百万并发到零延迟:Golang 玩转 Kafka / RabbitMQ / Redis 高效队列全攻略》,文章重点聚焦于高并发场景下如何稳定使用消息队列本身。

而在上一篇文章《订单重复扣款事故复盘:Go 微服务中事务发件箱模式的实战 (含源码)》中,我则结合一次真实线上事故,深入探讨了数据库与消息队列之间的一致性问题。

文章发布后,收到了许多有价值的反馈,其中几类问题反复被提及:

  • 消费者端不是应该做幂等吗?
  • 做了幂等,为什么还会出现重复扣款?
  • 事务发件箱算不算分布式事务?
  • 本地消息表和 Outbox 到底是什么关系?

这些问题共同指向一个核心议题:在微服务架构下,我们究竟应该如何理解“事务一致性”?

今天,我将以专题形式,将这些概念置于同一上下文中进行系统性梳理。我们更多关注设计原理,暂不局限于特定开发语言。

一、先说结论:幂等很重要,但它远远不够

在系统设计讨论中,常听到一句话:“这个问题没关系,消费者做幂等就好了。”

这句话对,但不完整

更准确的说法是:幂等只能防止“重复请求”,防不住“重复执行业务”。 许多线上事故恰恰由后者引发。前者容易被重视,而后者常被误认为是用户操作问题。实际上,严谨的程序员应更关注后者,这正是互联网“用户思维”的体现,关乎产品成败。

二、消费者幂等:它到底在防什么?

在微服务系统中,消费者幂等是必选项,这一点毋庸置疑,它非常有用。

消费者幂等通常解决以下问题:

  • MQ 至少一次投递导致的重复消息
  • 网络抖动、超时、重试造成的消息重发
  • 消费者实例重启、重复拉取消息

常见手段包括:

  • messageId / eventId 作为唯一键
  • 使用本地“已处理事件表”进行过滤
  • 用数据库唯一索引兜底
  • 采用 Redis 防重 Token(如 SETNX 命令)

但这里有一个非常重要的前提条件同一条业务事件,被重复投递。

三、为什么“做了幂等”,还是会重复扣款?

回顾上一篇文章的事故场景,实际情况是:

  • 上游订单服务因网络超时触发重试
  • 完整地重新执行了一次创建订单的业务逻辑
  • 数据库中生成两个不同的订单
  • 向 MQ 发送两条内容合法、语义正确的事件

对于下游服务(消费端)而言:

  • 订单 ID 不同
  • 事件 ID 不同
  • 请求语义完全合法

这不是重复消息,而是两次不同的业务操作。 在这种情况下,消费者幂等从设计上就无法阻止第二次执行

四、问题的根源:业务在生产者侧被执行了两次

这也是我在事故复盘中反复强调的观点:很多一致性问题,根本不在消费者,而在生产者。

如果同一次用户请求在生产者侧就可能被执行多次,那么:

  • 再完美的消费者幂等
  • 再稳定的 MQ

都只能起到“止损”作用,而非“治本”。在微服务架构中,各独立系统之间既不能完全信任上游,也不能过度依赖下游解决问题。尤其是生产端,若一味将问题抛给下游,问题可能愈发严重,因为下游系统对上游问题不可控,其所有操作仅是被动的防守。文章后面会提到的层层兜底才是正解。

五、那是不是要上“分布式事务”呢?

这是第二类高频问题。先给出一个工程结论:绝大多数互联网微服务场景,并不适合追求强一致的分布式事务。

能提出此问题的朋友,通常具备丰富的项目经验。从工程实践看,分布式事务大致有三条路线。

六、分布式事务的三种工程取舍

在实践项目中,工程取舍源于对各种资源的平衡考量。我们简要分析它们的特点及适用场景。

1️⃣ 强一致事务(2PC / XA)

  • 强一致
  • 性能与可用性成本极高
  • 适用于低并发、数据严谨的场景

在高并发、微服务体系中基本不推荐,即便在大厂互联网领域也较少使用。

2️⃣ 补偿型事务(TCC / Saga)

  • TCC 适合“钱”:因其 Try 阶段预留资源,能保证 Try 成功后,后续 Confirm 一定成功。
  • Saga 适合“流程”:例如电商下单等多步骤、耗时长且无法长时间锁定资源的流程。若后续步骤失败,则通过消息或补偿接口回滚前序操作。
    使用这两种模式时,幂等性是基础。若无幂等,补偿事务可能导致数据被“补偿过头”。
  • 业务侵入性强
  • 设计、开发与维护成本较高

适合场景

  • 金融系统
  • 核心账务
  • 对一致性要求极高的场景

3️⃣ 最终一致性事务(本地消息表 / Outbox)

最终一致性事务是当前分布式系统,尤其是高并发互联网架构中的主流选择。

  1. 本地消息表
    在业务数据库中增加一张“消息”表,从而在同一个业务事务中完成业务数据和消息表的插入。

  2. Outbox 模式(发件箱模式)
    Outbox 模式本质上是本地消息表在现代微服务中的“进化版”或标准称谓。其核心差异在于 OutboxEvent 表的设计,但原子操作、异步投递、短暂容错、最终一致的核心理念是相同的。

你在上一篇文章中看到的事务发件箱模式即属此类,此处不再赘述,感兴趣的朋友可回顾前文。

七、本地消息表 ≈ 事务发件箱吗?

这是第三个常被问到的问题。

结论很明确事务发件箱(Transactional Outbox),本质上就是“本地消息表”这类方案的模式化、规范化表达。

它们解决的是同一个问题:如何保证“业务数据写入”与“事件发布意图”处于同一个本地事务中。

区别更多体现在:

  • 命名语义
  • 模型抽象
  • 工程规范程度

而非设计思想本身,在某种意义上它们确实属于同一类方案。

八、事务发件箱真正解决的是什么?

以订单服务为例,事务发件箱保证:

  • 订单一旦成功创建
  • “需要发送一条业务事件”这个事实一定被记录
  • 不会因网络、MQ短暂不可用而丢失事件

它解决的是:生产者侧的一致性问题。

但它并不试图解决

  • 跨服务强一致
  • 自动回滚所有系统状态

这正是“最终一致性”的边界。

九、一个成熟系统的正确姿势:层层兜底

真正稳定的系统,从不依赖某个“银弹方案”,而是建立层层兜底的防御体系。

层级 解决的问题
前端 / API 防重复提交
生产者 防重复执行业务
本地事务 保证原子性
Outbox 防消息丢失
消费者 防重复消费
业务约束 最终兜底

我们不迷恋、不依赖、不拖泥带水,做好自己的本分。每一层只解决自己该解决的问题。

十、写在最后

回顾这些问题,其实并不复杂。许多“幽灵 Bug”并非源于代码能力不足,而是因为:对系统边界、责任划分和一致性模型的理解不够清晰。

当你真正厘清:

  • 幂等的适用范围
  • 生产者与消费者的职责边界
  • 最终一致性方案的取舍逻辑

许多线上事故本可提前避免。

如果你对相关实现细节感兴趣,可以回顾前两篇文章中的代码示例。也欢迎在评论区继续探讨你在幂等设计、事务一致性、微服务架构方面的实践经验。

后续我将继续围绕 Golang 微服务工程实践,系统性地梳理这些易被忽略却非常关键的问题。

本文写于平安夜,圣诞节发布,最后祝大家圣诞节快乐。
圣诞冰淇淋梗图
图:一个卡通风格的冰淇淋甜筒,寓意节日甜趣与技术探讨并存。


进一步探讨
本文探讨了微服务中事务一致性的核心挑战与分层解决方案。如果你对构建稳定、可靠的分布式系统架构有更多兴趣,可以访问云栈社区的后端与架构板块,这里汇聚了关于高可用、分布式系统设计的深度讨论。同时,社区也设有专门的Go语言板块,供Gopher们交流Goroutine、Channel等核心特性的实战心得。




上一篇:求职者薪资谈判要价18K过分吗?HR招聘策略引争议
下一篇:OpenSkills基础使用教程:为Cursor/Windsurf等AI助手安装跨平台技能
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 11:55 , Processed in 0.311824 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表