消息队列如何保证消息不丢失?
- 标签:消息队列、后端
🏛 业务架构师
消息不丢失要在三段链路各自设防,丢的位置决定防护动作:
生产端(网络丢、发送失败)
- 确认机制:RabbitMQ 用 confirm + return 回调;Kafka 设置
acks=all(全部 ISR 副本确认才算成功)+retries; - 发送失败要落地:重试超限后写本地表/死信,由补偿任务转投——不能只打日志。
Broker(宕机、刷盘丢)
- 持久化:RabbitMQ 队列与消息都 durable;Kafka 副本数 ≥2、
min.insync.replicas≥2、关闭 unclean 选举; - 集群化部署,单机 broker 的持久化承诺是有限的。
消费端(处理了但没 ack / ack 了但没处理)
- 手动 ack,处理成功后再确认(自动 ack = 处理前确认,挂了就丢);
- 但要防"处理失败无限重试":有限重试 + 死信队列 + 人工兜底。
架构视角的三点判断:
- 可靠性是有价格的:acks=all + 同步刷盘,吞吐降数倍——按消息价值分级配置:订单消息全防护,埋点日志尽力而为。一套集群全按最高等级配,是用钱换心理安慰;
- "不丢失"的最后防线是对账:所有防护叠加后仍有极小概率缝隙(极端故障组合),T+1 对账 + 差异自动修复是业务闭环的真正终点——金融场景没有"靠 MQ 保障资金一致"这种说法;
- 端到端 trace:消息 ID 全链路透传,丢了能定位丢在哪段——不可观测的可靠性等于不可验证。
一句话:生产确认、存储持久、消费手动 ack、对账兜底——四道闸门按业务价值选择性投资。
🏗 系统架构师
消息不丢失要在生产、存储、消费三段分别保证,链路上任何一段断了都前功尽弃:
生产端(防发送丢失):
- 同步发送或异步带回调确认;Kafka 配
acks=all(等待所有 ISR 副本落盘)+retries重试 + 发送缓冲区失败兜底日志; -RocketMQ 用同步发送 + SendResult 状态检查;本地消息表兜底:发失败落库,定时任务补发——重要业务我必配这个; - RabbitMQ 开 publisher confirm + mandatory + 备份交换机。
Broker(防存储丢失):
- Kafka:
replication.factor >= 3、min.insync.replicas >= 2,禁用 unclean leader 选举(非 ISR 副本不能当 leader); - RocketMQ 同步刷盘(SYNC_FLUSH)+ 主从同步复制(非异步);
- 单机 Raid/多副本,不迷信单盘。
消费端(防消费丢失):
- 手动 ack,处理成功后才确认;禁止自动提交 offset(自动提交 = 拿到就确认,处理一半宕机即丢);
- 消费失败重试 + 死信队列,杜绝无限重试阻塞;
- Kafka 手动提交 offset + 消费幂等(at-least-once 交付会重复,用幂等收敛到 exactly-once 效果)。
验证闭环:对账系统是最终防线——生产/消费计数对账,差异告警补数。架构认知:exactly-once 是昂贵的,at-least-once + 幂等消费是性价比最高的工程组合。
💻 全栈开发专家
消息不丢失 = 三段全设防:生产端、Broker、消费端。任何一段裸奔都白搭。
生产端(别发出去了就算完):
java
// RabbitMQ:confirm 模式,broker 确认落盘
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
if (!ack) resend(data); // 失败重投 + 本地记录
});
// Kafka:acks=all + 重试
props.put("acks", "all"); // 等所有 ISR 副本确认
props.put("retries", 3);
props.put("enable.idempotence", true); // 重试不产生重复关键:发送前先落业务库/本地消息表,发送失败可追溯重发,别让消息只活在内存里。
Broker 端:
- RabbitMQ:队列
durable=true,消息deliveryMode=2持久化; - Kafka:
replication.factor>=3、min.insync.replicas=2(acks=all 才有意义)、关闭 unclean 选举。
消费端(处理成功才确认):
java
// 关闭自动 ack,业务落库成功后手动确认
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
public void handleDelivery(...) {
saveToDb(body);
channel.basicAck(tag, false); // 失败 nack 重回队列/死信
}
});注意自动重试的死循环:毒消息反复失败会堵塞队列,要配最大重试次数 + 死信队列。
兜底闭环:消息表 + 定时对账(发出未确认/消费未回执的消息补发),我做对账系统时发现它能兜住 1% 的边角案例。三段设防 + 对账,才敢说"不丢"。