










用户反馈:刚才连续收到三条一模一样的推送通知。
日志里能查到这条消息,确实发送了三次——每次间隔大概 2-3 秒,内容完全一致。
消息队列默认保证"至少送达一次"(at-least-once),不保证"只送达一次"(exactly-once)。
消费者拉取消息 → 处理 → 发 ACK 给 MQ。如果在 ACK 之前:
MQ 会把消息重新投递给其他消费者(或同一消费者),于是同一条消息被消费了多次。
推送服务如果不做幂等,就会推送多次。
每条推送消息在生成时分配一个唯一的 msgId。推送服务消费时,用 Redis 记录"这个 msgId 已处理过":
public void consumePushMessage(PushMessage msg) {
String dedupKey = "push:sent:" + msg.getMsgId();
// setIfAbsent: 只有第一次会返回 true
Boolean isFirst = redis.setIfAbsent(dedupKey, "1", 24, TimeUnit.HOURS);
if (!Boolean.TRUE.equals(isFirst)) {
log.info("Duplicate push message {}, skip", msg.getMsgId());
return;
}
// 真正发推送
pushGateway.send(msg.getDeviceToken(), msg.getTitle(), msg.getBody());
}
setIfAbsent 是原子操作,并发情况下只有一个线程能成功写入,其余线程走去重逻辑。
TTL 设为 24 小时足够覆盖 MQ 的重投窗口——正常情况下 MQ 不会延迟 24 小时才重投。
msgId 必须在生产端就确定,不能在消费端生成——消费端每次消费生成一个新 ID,去重就失效了。
常见方案:
orderId:123456:PAIDmessageId(Kafka 没有内置,RocketMQ 有 msgId,但 RocketMQ 的 msgId 在 broker 重启后可能重置,业务层 ID 更可靠)这不是偶然的,说明消费端的 ACK 超时时间配置得偏短,或者推送网关响应慢导致消费超时被重投。
排查步骤:
幂等去重是根本解;优化超时配置减少不必要的重投是辅助。两件事都要做。
设备侧也可以做最后一道防线:App 在本地记录最近 N 条 msgId,收到重复的 msgId 直接静默丢弃,不展示给用户。但这是 App 的职责,后端不能依赖客户端来兜底。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。