node.js中单纯用try/catch或内存set无法防止重复消费,因其单线程异步模型不共享进程间状态,集群下new set()仅限当前worker有效;消息重投时若无跨实例幂等校验(如redis setnx+ex),业务逻辑可能重复执行。

为什么 Node.js 里单纯靠 try/catch 或内存 Set 挡不住重复消费?
Node.js 单线程 + 异步 I/O 的模型,天然不共享进程间状态。你用 new Set() 存已处理的 msgId,只在当前 worker 实例内有效;集群部署下,另一个进程完全看不到这个集合。网络抖动、消费者重启、channel.ack() 调用失败或超时,都会触发 RabbitMQ 重投——此时若没跨实例校验,update balance set amount = amount + 100 就真会执行两次。
msgId 必须由生产者生成并透传,不能在消费者端拼接
重复消费防护的前提是消息身份可识别、全局唯一。如果消费者自己用 Date.now() + Math.random() 生成 ID,那同一条原始消息被重发两次,就会得到两个不同 ID,锁机制直接失效。
- 生产者必须在发送时通过
message.properties.messageId设置真实业务 ID(如订单号order_123456)或 UUID - RabbitMQ 客户端(如
amqplib)需确保该字段随消息体完整透传到消费者 - 消费者拿到的
msg.fields.routingKey或msg.properties.headers不能替代msg.properties.messageId,后者才是 AMQP 标准定义的唯一标识字段
用 redis.setnx() 做幂等入口,但得加过期时间防死锁
Redis 的 SETNX(即 SET key value NX)是原子操作,适合做“首次写入才成功”的判断。但只用它还不够:如果消费者在加锁后崩溃、未执行业务也未删 key,这个锁就永远卡住。
根据 package.json 中的 engines 字段自动检测 Node.js 版本需求,未安装 nvm 时自动安装并切换至对应版本。支持 Windows、macOS、Linux。适用于 Node.js 项目开发、版本切换和环境配置。
- 必须搭配
EX参数设置 TTL,例如redis.set(key, '1', 'NX', 'EX', 300)(5 分钟过期) - key 命名建议带业务前缀和环境标识,比如
mq:dedup:prod:order_123456,避免测试/生产环境 key 冲突 - 不要用
redis.get() + redis.set()两步模拟,这不是原子的,高并发下必然漏锁
ACK 时机必须严格放在业务完成且 Redis 写入之后
很多人把 channel.ack() 放在 try 块开头或中间,结果业务出错、Redis 写失败,但消息已被确认,下次重投时又走一遍——等于白防。
- 正确顺序:收到消息 → 提取
msg.properties.messageId→redis.set(...NX...EX...)→ 成功则执行业务逻辑 → 业务成功后再调用channel.ack() - 如果
redis.set()返回null(表示已存在),直接channel.ack()跳过,不执行业务 - 如果
redis.set()报错(如连接中断),应channel.nack(msg, false, true)让消息重回队列尾部,而不是丢弃或跳过
真正难的不是写几行 redis.setnx,而是把 msgId 的源头、Redis 锁的生命周期、ACK 的位置这三件事串成一条不可分割的链。任意一环脱钩,幂等就形同虚设。










