news 2026/5/11 9:25:31

穿透 MQ 专栏 (三):【幂等防御】“网卡了一下,用户被扣了两次钱?”:如何防住防不胜防的重复消费

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
穿透 MQ 专栏 (三):【幂等防御】“网卡了一下,用户被扣了两次钱?”:如何防住防不胜防的重复消费

在上一篇,为了防住“薛定谔的消息”,我们在生产者、MQ 服务端和消费者身上绑上了“生死契约”(手动 ACK、副本落盘、无限重试)。看着坚不可摧的消息防线,你终于睡了个安稳觉。

但就在第二天凌晨,客服主管再次一脚踹开你的房门:“有个暴躁老哥在投诉!他明明只买了一双鞋,但网卡了一下,系统竟然连续扣了他两次钱!

你赶紧爬起来查日志,瞬间冷汗直冒:扣款系统确实只发起了一次请求,但 MQ 里竟然躺着两条一模一样的扣款消息!消费者(订单系统)傻乎乎地拿了一条,扣了 100 块;紧接着又拿了下一条,又扣了 100 块。

“消息是没丢,但它踏马的发重了啊!”

今天,我们就来直面这个把无数初级程序员坑得体无完肤的物理常态——消息重复。并手撕大厂高频面试题:什么是幂等性?高阶开发是如何在代码里穿上“防弹衣”的?


一、 MQ 的摆烂哲学:为什么重复消费防不胜防?

很多新手对 MQ 有一种不切实际的幻想,认为 MQ 内部应该有一种机制,能帮我把重复的消息自动过滤掉。

放弃幻想吧!在分布式的物理世界里,只要有网络,只要你还想要“高可靠性”,重复发消息就是不可避免的死结。

我们在上一篇埋下的那个致命场景,就是重灾区:

  1. 扣款系统(生产者)成功把消息发给了 MQ。

  2. MQ 稳稳地把消息存进了磁盘,并高高兴兴地给扣款系统发回执(ACK)。

  3. 就在这 0.1 秒的瞬间,光缆被挖掘机挖断了,或者网卡抖动了一下。

  4. 扣款系统迟迟等不到回执,它心想:“完了,消息肯定在半路丢了!”

  5. 尽职尽责的扣款系统触发了重试机制,又把这条消息发了一遍!

面对这两条一模一样的消息,MQ 是怎么想的? Kafka 和 RocketMQ 的底层哲学非常直接,叫做At Least Once(至少一次)

“兄弟,我只能向你保证消息绝对送达(至少一次),但我绝不保证我不会送两遍。至于怎么过滤重复,那是你业务系统该干的脏活累活,别赖我!”

所以,面对重复消息,不要试图在 MQ 层面堵,必须在你的消费者代码里防。


二、 说人话的“幂等性(Idempotency)”

为了解决重复消费,计算机界发明了一个极其拗口、听起来像某种法术的数学名词——幂等性

别去背百科上的定义,咱们直接上“泥土气息”的业务类比:

  • 什么是【不幂等】?(极其危险)就像老式的电灯拉线开关。你拉一次,灯亮了;再拉一次,灯灭了。 在代码里,就是UPDATE account SET balance = balance - 100。 这条 SQL 执行 1 次和执行 2 次,用户的余额是完全不一样的!如果消息发重了,用户直接破产。

  • 什么是【幂等】?(绝对安全)就像带刻度的电灯旋钮。你把旋钮拧到“开”的位置,灯亮了;如果你的手抽筋了,又连续往“开”的位置拧了 10 次,灯依然只是亮着,不会爆炸。 在代码里,就是UPDATE account SET balance = 500 WHERE balance = 600不管这条相同的消息被 MQ 投递了 1 次还是 100 次,最终业务数据库里的状态,和被投递 1 次一模一样。这就叫幂等。


三、 高阶研发的“防重装甲”:实战三大解法

明白了概念,接下来才是重头戏。如何在消费者代码里,把所有操作都改造成“幂等”的? 大厂架构师通常有三种防重手段,从青铜到王者,层层递进。

1. 青铜解法:数据库唯一索引(Unique Key)硬抗

这是最简单粗暴、也是见效最快的兜底防线。

  • 核心逻辑:建立一张独立的日志表msg_idempotent_log。给每一条消息生成一个唯一的业务 ID(比如order_id + 动作=10086_PAY),把这个字段设为唯一索引(Unique Key)

  • 业务流程:消费者拿到消息后,先去INSERT这张表。

    • 如果INSERT成功,说明是第一次来,继续执行扣款业务。

    • 如果INSERT报错(触发DuplicateKeyException唯一冲突),直接捕获异常,打印一句log.warn("重复消息,直接丢弃"),然后向 MQ 返回成功 ACK,放行!

  • 缺点:所有的并发压力全部砸在了数据库的INSERT上,高并发场景下,数据库的锁竞争会成为性能瓶颈。

2. 白银解法:RedisSETNX(极速拦截机)

既然数据库扛不住并发,我们就把防重前线推到内存里,用 Redis 做分布式锁拦截。

  • 核心逻辑:消费者拿到消息,先去 Redis 执行一句SETNX msg_id 1(只有当 key 不存在时才设置成功)。

  • 业务流程:

    • 如果 Redis 返回 1(成功),说明是新消息,去执行扣款入库。

    • 如果 Redis 返回 0(失败),说明这是重复消息,直接把消息扔掉,返回 ACK。

  • 致命的隐患(锁的释放问题):这种方案看似完美,其实暗藏深坑! 如果 Redis 返回了 1,但在执行后续的扣款数据库操作时,数据库宕机报错了!此时业务没做完,但 Redis 里已经留下了防重的标记。当 MQ 几秒后发起重试把消息再次投递过来时,Redis 会直接把它当成重复消息无情抛弃。结果:消息永远无法被正确消费,业务卡死。(解决这个坑需要引入极其复杂的分布式锁过期时间、状态反查机制,代码复杂度陡增。)

3. 王者解法:业务状态机(Optimistic Locking 终极优雅)

真正的顶级架构师,从来不喜欢引入多余的组件(既不建新表,也不用 Redis)。最优雅的幂等,一定是融合在业务逻辑本身之中的。

在电商系统中,任何一笔订单都一定有“状态(Status)”。比如:1=待支付,2=已支付,3=已发货。 这就构成了天然的状态机防重装甲

  • 核心逻辑:消费者拿到扣款成功的消息(想要把订单状态改为已支付),不需要查 Redis,不需要查防重表,直接去 MySQL 执行一条带前置状态条件的 UPDATE 语句

    SQL
    UPDATE orders SET status = 2, update_time = NOW() WHERE order_id = '10086' AND status = 1; -- 【关键防御:只能从“待支付”状态流转】
  • 奇迹时刻推演:

    • 第一条消息到来:数据库里status确实是 1。SQL 匹配成功,affected_rows = 1。订单状态变成了 2。

    • 网卡了一下,重复的第二条消息到来:同样执行这句一模一样的 SQL。但是!此时数据库里的status已经是 2 了。WHERE status = 1这个条件直接落空。MySQL 会悄无声息地返回affected_rows = 0

    • 代码处理:你的 Java 代码判断一下,如果影响行数为 0,说明状态已经被改过了,这就是重复消息!直接return,大摇大摆地向 MQ 发送 ACK。

不需要任何额外的中间件,仅仅利用了业务数据自带的状态转换单向性,配合 MySQL InnoDB 行锁的天然原子性,就完美斩断了重复消费的魔爪。


💡 灵魂拷问:为下一篇埋下天坑

经过这三篇的浴血奋战,我们用 MQ 挡住了洪峰(削峰),用确认机制防住了丢失(可靠性),用状态机防住了扣两次钱(幂等性)。

你的秒杀系统现在简直像是一个刀枪不入的铁桶。

但是,老板又来找麻烦了。 这次的案发现场更加诡异:“有个用户下了单,然后立刻申请了退款。结果我们的系统不仅没给他退款,反而把货给他发出去了!

你调出日志,看到了让人三观崩塌的一幕: 扣款系统明明是先发了“订单创建”的消息,后发了“订单取消”的消息。 但在消费者(订单处理系统)那边,竟然是先收到了“订单取消”,后收到了“订单创建”! 系统一看,订单还没创建怎么取消?直接把“取消”消息报错扔了。紧接着“创建”消息到了,系统高高兴兴地把货给发了。

“这消息是怎么在网络里超车的?说好的先来后到呢?”

如果在高并发场景下,消息乱序了,所有的业务状态机都会瞬间崩溃。 要想让消息绝对有序,MQ 只能退化成单线程运行,那吞吐量就直接跌回原始时代。

在“极致的性能”与“绝对的秩序”之间,MQ 究竟是如何妥协的? 下一篇,我们将直击分布式系统的心脏痛点:《【秩序之争】被杀死的全局顺序:消息乱序与千万级积压的救火指南》

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/11 9:24:36

如何免费获得专业音质:Equalizer APO音频均衡器完整教程

如何免费获得专业音质:Equalizer APO音频均衡器完整教程 【免费下载链接】equalizerapo Equalizer APO mirror 项目地址: https://gitcode.com/gh_mirrors/eq/equalizerapo Equalizer APO是一款功能强大的免费开源系统级音频均衡器软件,能够实时处…

作者头像 李华
网站建设 2026/5/11 9:21:39

Structured Output工程2026:让LLM稳定输出JSON的完整实战指南

让LLM输出合法JSON听起来简单,实际生产中却是无数Bug的来源。本文系统梳理结构化输出的所有主流方案,从基础技巧到生产级实现,帮你彻底解决LLM输出不稳定的问题。一、为什么结构化输出如此重要在AI应用开发中,LLM的输出通常需要被…

作者头像 李华
网站建设 2026/5/11 9:21:38

ncmdump终极指南:快速解锁网易云音乐NCM加密文件的完整解决方案

ncmdump终极指南:快速解锁网易云音乐NCM加密文件的完整解决方案 【免费下载链接】ncmdump ncmdump - 网易云音乐NCM转换 项目地址: https://gitcode.com/gh_mirrors/ncmdu/ncmdump 当你下载了网易云音乐的付费歌曲,却发现只能在特定播放器里收听&…

作者头像 李华
网站建设 2026/5/11 9:21:36

【2026实测】直击Turnitin算法:英文论文AI率97%降至8%的4种高效方法

大家最近都在为英文降aigc率发愁吧,作为研三党,我太懂这种痛了,之前我自己写英文初稿,写完直接拿去查重,结果turnitin检测ai率飙到了89%,当时看着报告整个人都懵了。 怎么给英文降ai?对于非母语…

作者头像 李华
网站建设 2026/5/11 9:21:35

从暴力到优雅:LeetCode 3. 无重复字符的最长子串 深度解析

📌 前言:为什么它是面试之王?在 LeetCode 的“Hot 100”榜单中,第 3 题《无重复字符的最长子串》​ 绝对是面试界的“钉子户”。这道题看似简单,实则考察了:双指针思想(滑动窗口的本质&#xff…

作者头像 李华
网站建设 2026/5/11 9:20:33

AI加持下的泳装创新,你了解“先知大模型”能做什么吗?

随着春夏泳装市场竞争日益激烈,北京先智先行科技有限公司通过“先知大模型”“先行 AI 商学院”和“先知 AIGC 超级工场”,为品牌设计提供了全链路智能化支持。先知大模型能够快速分析市场数据,优化设计方案,提升生产效率&#xf…

作者头像 李华