消息推送防重系统:多渠道消息不重不漏#
业务里最常见的需求:“订单发货了,提醒一下用户”——但这条提醒可能要走短信、公众号、App 推送三个渠道,还可能被定时任务重复触发。怎么保证用户收到一次,且一定收到一次? 这是多渠道消息防重系统的核心问题。
一、问题拆解#
目标:同一事件同一用户,每个渠道恰好一条;失败的渠道可重试,但重试不重发。
二、架构#
三、核心设计#
1. 幂等键(防重的钥匙)#
1
2
3
4
5
6
7
8
|
{
"biz_type": "ORDER_SHIPPED",
"biz_id": "order-20260907-001", // 业务唯一
"user_id": "u-123",
"channel": "SMS" // 渠道维度
}
// 幂等键 = biz_type + biz_id + user_id + channel
// 同一键只允许一条成功记录
|
关键:幂等键必须确定性生成(重试时用同一个键),不能随机。
2. 渠道去重(DB 唯一索引)#
1
2
3
4
5
6
7
8
9
|
CREATE TABLE push_task (
id BIGINT PRIMARY KEY,
idempotent_key VARCHAR(128) NOT NULL,
channel VARCHAR(32) NOT NULL,
status TINYINT, -- 0待发/1成功/2失败/3终态
...
UNIQUE KEY uk_idempotent (idempotent_key) -- 防重兜底
);
-- 插入冲突(重复任务)→ 跳过,不重发
|
DB 唯一索引是防重的最后防线(哪怕代码层漏了,DB 也能拦)。
3. 渠道抽象#
1
2
3
4
5
6
7
8
9
10
|
type Channel interface {
Send(ctx, msg) error
Name() string
}
type SMSChannel struct{} // 短信渠道
type WechatChannel struct{} // 公众号渠道
type AppChannel struct{} // App 推送
// 新渠道 = 实现接口 + 注册(开闭原则)
|
4. 失败重试与补偿#
四、踩坑记录#
- 幂等键不含渠道:三个渠道共用一个键 → 发完短信,公众号被去重了 → 键必须含渠道维度
- 重试不换键:失败重试时重新生成键 → 每条重试都"看起来是新消息" → 重试必须复用原幂等键
- DB 唯一索引冲突不处理:插入冲突直接报错 → 业务炸 → 冲突捕获后按"已存在"处理(幂等跳过)
- 渠道 API 返回"成功"但没发出去:渠道半成功(状态不确定)→ 用对账任务定期核对渠道回执
五、监控与指标#
消息推送防重的核心认知:
- 幂等键 = biz_type + biz_id + user_id + channel:确定性生成,含渠道维度
- DB 唯一索引是最后防线:代码漏了,DB 拦
- 失败要分可重试/不可重试:退避重试 + 告警 + 降级补偿
- 对账是"不遗漏"的保证:渠道回执核对,确认送达
推送系统的口碑 = 不打扰(不重)+ 不缺席(不漏)。 幂等防重 + 失败补偿 + 对账,三件套齐了才敢说"推送可靠"。
微信公众号「福清而不淡」
后端架构 · AI 工程 · 云原生的一线实践,扫码关注,不错过更新。
本文已同步发布到公众号,欢迎留言交流。