如何保证并发和分布式消息消费的业务幂等?
未标记
考查重点:动作去重与原子状态迁移。业务动作 ID 配唯一约束与原子状态迁移,重复返回既有结果,外部渠道另对账
本题目录
参考答案
核心回答
消息消费的业务幂等,是让同一业务动作被重复或并发触发时,不产生额外的业务副作用。重点是业务状态的约束和失败恢复,不能只靠消费者“先查一下是否处理过”。
通常按以下流程设计:
- 定义稳定的业务幂等键:例如订单创建请求号、支付流水号、短信任务号,并区分动作类型和消费方。重试同一动作复用同一个键,不能每次都生成新号。
- 原子判断并执行:通过数据库唯一约束、条件更新或状态机决定谁能处理,避免两个消费者都查到“未处理”后一起执行。
- 本地事务一起提交:去重记录、相关业务修改和本步成功结果放在同一个数据库事务中,避免只写了“已处理”却没完成业务,或业务完成却没有记录。
- 提交后再确认消费:成功提交后才
ACK或提交消费进度;若提交后确认前宕机,重投时读取已提交结果,不再重复产生业务效果。 - 外部副作用单独处理:短信、支付等远程调用不能直接和本地数据库事务原子提交,需要下游幂等请求号、结果查询和持久任务恢复等能力配合。
进程内锁只协调本实例,Redis 锁可以减少多实例竞争,但都不能替代持久业务约束;消息系统的投递保证也不能直接延伸成“外部业务永远只执行一次”。
为什么先查询、再处理不够?
假设两个消费者同时收到同一个下单事件,都查到没有创建订单,然后各自插入,普通查询本身没有排他性。
可以给订单创建请求号加唯一约束:同一请求并发插入时,由数据库决定谁成功。也可以让任务状态通过条件更新,从“待处理”转成“处理中”,只有成功完成状态转换的执行者继续。重点是把检查与占有执行资格合成一个原子动作,而不是把两条普通操作写在相邻位置。
幂等键要表示业务意图。例如一次订单创建与一次短信发送是两个动作,不应只用同一个订单号把它们互相去重;用户确实再次下单,也应有新的请求号。相同幂等键却带了不同金额、收件人等关键参数时,应识别冲突,不能静默当作同一次操作。
本地事务和消费确认怎样衔接?
以同库订单修改为例,可以在一个事务中插入带唯一约束的消费记录、修改订单并记录成功结果。并发重复请求发生唯一键冲突时,要确认已有的成功结果,再按重复完成处理;普通数据库异常不能一律当成重复消息吞掉。
三个失败位置需要分清:
- 本地事务提交前失败:业务和去重记录一起回滚,消息重投后可以重新执行。
- 事务已提交,
ACK前失败:业务已完成,重投会命中持久记录,跳过副作用后重新确认。 - 先
ACK,再执行业务时失败:消息系统可能已经不再投递,本次业务就可能丢失,因此不能把确认放在可靠处理之前。
若将“处理中”状态单独提交来认领长任务,还需要记录执行归属、尝试次数或版本,并设计超时恢复。看到“处理中”只能说明曾被接管,不能证明业务成功;重新认领时也要防止旧执行者恢复后继续提交结果。
下单后发短信,怎样避免重复发送?
可以先在订单事务中创建具有唯一任务号的持久短信任务,把“订单成功”和“需要发送短信”一起记录下来。若 MQ 消费职责是可靠登记这个任务,登记事务提交后即可确认消费;独立发送器再认领任务、调用短信平台并记录结果。
不过,这只能可靠保存发送意图,不能消除远程调用的不确定性:短信平台可能已接受发送,但成功响应丢失;发送器重启后只看到本地还没标成功,直接重发就可能重复。
如果平台支持幂等请求号,应让同一短信任务的重试复用该号,并确认它的去重范围与保留期限;支持可靠结果查询时,可先查询并恢复本地状态。若平台既不能去重、也无法查清结果,仅靠本地锁和状态表,就无法同时保证绝不重复、绝不漏发,需要将不确定任务纳入对账和明确的重试策略。
常见追问
Redis SET NX 去重可以吗? 可以作为短期拦截,但它与数据库修改分属不同系统。先写去重键后业务失败,可能错误拦住后续重试;业务成功后还没写去重键就宕机,又可能重复处理。TTL 到期、故障切换或缓存丢失也会让历史去重信息消失,重要业务仍应有持久约束。
去重记录保存多久? 至少覆盖可能的延迟投递、重试、死信重放和业务补发范围。没有通用 TTL;超过窗口的历史重放,应仍能被业务唯一约束、版本或状态机识别,不能删完记录就默认安全。
幂等也能解决乱序吗? 不能直接解决。两条不同的事件都可能只执行一次,却按错误顺序把新状态覆盖成旧状态;还需要按业务版本、允许的状态转换或顺序处理规则约束。
参考资料
- 国内:JavaGuide:接口幂等性设计详解:业务唯一键、去重表和状态机部分说明怎样把去重与业务修改放进同一事务,以及锁为什么不能代替历史状态判断。
- 国内:advanced-java:如何保证消息不被重复消费?:消费进度尚未确认就重启的例子解释消息为什么会重投,并通过数据库唯一键说明去重应落到业务数据上。
- 国内:阿里云:消息幂等:借鉴发送重试、应答丢失与重平衡的说明,解释正文中重复消费的来源及稳定业务标识的必要性。
- 国外:AWS:Making retries safe with idempotent APIs:调用方请求号、原子记录、迟到请求和参数冲突部分说明幂等键如何表达意图,以及保存期限和不确定结果如何影响重试。
- 国外:RabbitMQ:Reliability Guide:消费确认、生产端重发和消费者重投部分说明 ACK 的作用,以及为什么处理方仍需设计幂等业务逻辑。
相关面经
19 篇启云方
AI 应用开发工程师Kafka 如何避免重复消费?业务幂等怎么做?
虎牙
全栈开发如何防止 MQ 的消息重复消费?
字节跳动
后端开发这里消息队列起到什么作用,如何解决消息重复消费的问题
欧达光电
信息管培生MQ异步消费怎么保证幂等?异常情况下答题轮次怎么恢复?
小红书
Agent 开发工程师你用 MQ,并通过任务 ID 和状态判断命令幂等;如果发送端一个、接收端多个,会怎样?
展开剩余 14 篇面经
水滴
AI全栈猎聘
AI 应用开发工程师Q13. 你的异步结算链路、消费幂等、失败异常和并发抢占是怎么做的?
老虎国际
后端开发如何避免重复消费?
极兔速递
后端开发工程师Redis Stream当作消息队列是怎么保证消费幂等?
汇川技术
全栈开发工程师怎么保证MQ发消息丢不了,怎么保证幂等性
字节跳动
AI 全栈开发工程师比如现在 MQ 发了很多条一样的消息, 你怎么去做幂等?
阿里巴巴
AI 应用研发工程师蚂蚁集团
蚂蚁集团
如何保证消息幂等性
阿里巴巴
AI 全栈开发工程师你俩都用了,我就问一些通用的。如果现在有个消息队列要去接消息处理一些业务逻辑,怎么保证业务的幂等性?(追问·业务幂等)业务上怎么做幂等的?(下单发短信场景)(追问)你两个消息同时去查这个 task,task 不都是在执行中吗?怎么防这个判断?并发?(追问)用 JUC 的一些变量、CAS 这种?(追问)分布式场景用 Redis?(追问)完整流程是什么?先用 Redis 锁住,然后执行业务,执行完释放锁?一锁二判三更新是吧?
快手
Agent 应用开发工程师MQ 消费幂等一般怎么实现?(你项目里是怎么做的)
阿里巴巴
AI 全栈开发工程师消息队列怎么防止重复消费消息
拼多多
后端开发工程师消息队列顺序消费 重复消费 消息丢失怎么解决
网易
AI 应用开发工程师RabbitMQ消息丢失、重复消费、堆积、阻塞的成因和线上解决方案。










