很多面试Java后端岗位的人都会遇到这道经典面试题,今天我也来讲讲我对这道题的理解。
其实,面试官并不单单想听你背八股文说出“持久化”、“手动ACK”这些关键词;我在面试中直接被问:你实际项目中是怎么做的,配置怎么配,代码怎么写的等这些实际问题。面试官是想知道:
- 你是否真正理解消息从生产到消费的完整链路?
- 你是否在实际项目中踩过坑、解决过问题?
- 你是否能给出系统性的解决方案,而不是零散的知识点?
所以,回答这个问题的关键不是简单的堆砌概念,而是要展示你的全链路思维。
一条消息从生产者发出到被消费者消费,需要经历三段路:
生产者 → 交换机 → 队列 → 消费者,每一段都有可能丢失消息,对应三个核心问题:
- 生产者 → 交换机:消息发出去了,但是交换机不存在或者路由规则配置错误,消息直接丢了,而生产者这边却不知道。
- 交换机 → 队列 →Broker存储:消息到了Broker,但是Broker宕机重启,内存里的消息全没了。
- 队列 → 消费者:消费者拉取到了消息还没处理完就崩了,但默认自动ACK,消息已经被删除了。
第一关:生产者端——让每条消息都有"回执"
如果面试官追问:“生产者怎么知道消息有没有发送成功?"
这时候你要抛出两个核心机制:Publisher Confirm和Return Callback。
Publisher Confirm:消息到达交换机后,Broker会给生产者一个确认(ack=true)或拒绝(ack=false)。
Return Callback:消息到了交换机,但是没有匹配到任何队列时,Broker会把消息退回给生产者。两者配合,就能确保生产者端不会丢失消息。
在代码层面,先进行配置:
spring: rabbitmq: publisher-confirm-type: correlated # 开启Confirm4 publisher-returns: true # 开启Return然后配置回调:
@Configuration@Slf4jpublicclassRabbitConfig{@AutowiredprivateRabbitTemplaterabbitTemplate;@PostConstructpublicvoidinit(){rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{if(ack){log.info("消息已经到达交换机,id={}",correlationData.getId());//更新本地消息表状态("已送达")}else{log.info("消息未到达交换机,cause={}",cause);//触发重试或告警}});rabbitTemplate.setReturnsCallback(returnedMessage->{log.error("消息未路由到队列, exchange={}, routingKey={}",returnedMessage.getExchange(),returnedMessage.getRoutingKey());// 记录到数据库,后续人工处理});}}发送消息时别忘了设置 mandatory=true,否则Return回调不会触发
rabbitTemplate.setMandatory(true);rabbitTemplate.convertAndSend("order-exchange","order.create",messageBody,newCorrelationData(UUID.randomUUID().toString()));第二关:Broker端——三重持久化,一个都不能少
面试官追问:“如果Broker挂了怎么办?”
这时候可以告诉面试官:Broker要做到消息不丢失,需要三样东西同时持久化
| 要素 | 配置 |
|---|---|
| 交换机 | durable = true |
| 队列 | durable = true |
| 消息 | deliveryMode = 2 |
@BeanpublicQueueorderQueue(){returnQueueBuilder.durable("order-queue").withArgument("x-dead-letter-exchange","dlx-exchange").withArgument("x-dead-letter-routing-key","dlx-routing-key").build();}第三关:消费者端——手动ACK + 死信队列
面试官追问:“如果消费者处理到一半突然挂了怎么办?”
这里是最容易丢失消息的环节,也是开发最容易踩坑的地方。默认情况下,消费者是自动ACK的,消息一拉取到,还没处理完,Broker就认为已消费并删除消息,如果此时消费者突然挂了,那么消息就会永久丢失。
解决方案:关闭自动ACK+手动确认
pring:rabbitmq:listener:simple:acknowledge-mode:manual# 关闭自动ACK6prefetch:1# 每次只拉一条,防止消息堆积消费者代码
@RabbitListener(queues="order-queue")publicvoidhandleOrder(Messagemessage,Channelchannel)throwsIOException{longdeliveryTag=message.getMessageProperties().getDeliveryTag();try{Stringbody=newString(message.getBody(),StandardCharsets.UTF_8);Orderorder=JSON.parseObject(body,Order.class);// 幂等校验:防止重复消费if(isAlreadyProcessed(order.getOrderId())){channel.basicAck(deliveryTag,false);return;}// 执行业务逻辑stockService.deductStock(order.getGoodsId(),order.getQuantity());// 业务成功,手动ACKchannel.basicAck(deliveryTag,false);}catch(Exceptione){log.error("消费失败",e);// 重试次数控制IntegerretryCount=message.getMessageProperties().getHeader("x-retry-count");intcurrent=(retryCount==null)?0:retryCount;if(current<3){// 未超限,重新入队message.getMessageProperties().setHeader("x-retry-count",current+1);channel.basicNack(deliveryTag,false,true);}else{// 超限,拒绝消息,进入死信队列log.error("重试耗尽,转入死信队列");channel.basicNack(deliveryTag,false,false);}}}终极兜底:本地消息表
面试官如果继续追问:“如果Confirm回调本身也丢了呢?”
这时候我们要说出终极方案——本地消息表,核心思路很简单:
- 业务数据和消息记录在同一个本地事务中写入数据库。
- 定时任务轮询“待发送”状态的消息,调用MQ发送。
- 发送成功后更新为“已发送。
- 如果失败,则进入重试;超过重试次数标记为“失败”,人工介入。
@Transactional(rollbackFor=Exception.class)publicvoidcreateOrderReliably(Orderorder){// 1. 业务入库orderMapper.insert(order);// 2. 消息记录入库(同事务!)MessageLoglog=newMessageLog();log.setMsgId(UUID.randomUUID().toString());log.setMsgBody(JSON.toJSONString(order));log.setStatus("PENDING");messageLogMapper.insert(log);}@Scheduled(fixedDelay=30000)publicvoidretryPendingMessages(){List<MessageLog>pending=messageLogMapper.selectByStatus("PENDING");for(MessageLogmsg:pending){if(msg.getRetryCount()>=5){messageLogMapper.updateStatus(msg.getMsgId(),"FAILED");continue;}``` rabbitTemplate.convertAndSend("order-exchange","order.create",msg.getMsgBody(),newCorrelationData(msg.getMsgId()));messageLogMapper.incrementRetryCount(msg.getMsgId());}}本地消息表的作用在于:把“发消息”这个动作从同步调用变成了“异步补偿”,即使MQ短暂不可用,消息也不会丢失。
综上,这个面试题可以作如下回答:
” 消息从生产到消费有三个环节可能丢失,我的方案是全链路闭环:
生产者端: 我开启了Publisher Confirm和Return Callback,确保消息到达交换机,未路由的消息也能被回收处理。对于核心业务,还会配合本地消息表做最终兜底。
Broker端: 我确保交换机、队列、消息三者都做了持久化,生产环境使用集群模式避免单点故障。
消费者端: 我关闭了自动ACK,改为手动确认,业务处理成功后才ACK。处理失败的消息会有限次重试,重试耗尽后转入死信队列,避免消息丢失和毒消息循环。同时业务层做好幂等处理,防止重复消费。
这套方案在我们项目中已经跑了很久,核心业务消息零丢失。“
【Java笔记 小李版】,我们一起进步!