消息推送防重系统:多渠道消息不重不漏

业务里最常见的需求:“订单发货了,提醒一下用户”——但这条提醒可能要走短信、公众号、App 推送三个渠道,还可能被定时任务重复触发。怎么保证用户收到一次,且一定收到一次? 这是多渠道消息防重系统的核心问题。

一、问题拆解

一 风 次 险 业 : 1 2 3 务 . . . 事 件 重 遗 错 ( 复 漏 发 订 : : : 单 定 渠 渠 发 时 道 道 货 任 失 配 ) 务 败 置 → 重 ( 错 跑 短 要 信 → 推 → 欠 送 费 内 多 用 / 容 个 户 公 发 渠 收 众 错 道 号 渠 ( 3 道 短 t 信 遍 o / " k 公 发 e 众 货 n 号 提 / 醒 过 A " 期 p ) p → ) 用 户 没 收 到

目标:同一事件同一用户,每个渠道恰好一条;失败的渠道可重试,但重试不重发。

二、架构

业 推 渠 重 务 送 道 试 事 编 执 调 件 │ ▼ 排 │ │ ▼ 行 │ │ │ ▼ 度 服 器 ( 务 ( 失 ( 1 2 短 3 4 5 败 事 . . 信 . . . 任 件 务 总 生 按 / 发 调 结 定 线 成 渠 送 用 果 时 / 消 道 公 前 渠 落 重 R 息 拆 众 查 道 库 试 a 任 分 号 重 ( ) b 务 任 ( A 成 b ( 务 / 幂 P 功 i 消 等 I / t 息 A 键 失 M I p 败 Q D p + / ) ) 待 + 渠 重 道 试 渠 ) ) 道 列 表 + 幂 等 键 )

三、核心设计

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. 失败重试与补偿

失 补 败 偿 类 可 不 : 某 最 型 重 可 个 终 区 试 重 渠 确 分 ( 试 道 认 : 渠 ( 一 : 道 参 直 用 超 数 失 户 时 错 败 " / 误 ( 一 限 / 如 定 流 欠 短 收 ) 费 信 到 → ) 欠 " → 费 用 退 ) 回 避 终 → 执 重 态 ( 试 降 渠 ( + 级 道 5 : 回 次 告 换 传 / 警 备 送 递 人 用 达 增 工 渠 状 间 道 态 隔 ( ) ) A p p 推 送 补 发 )

四、踩坑记录

  1. 幂等键不含渠道:三个渠道共用一个键 → 发完短信,公众号被去重了 → 键必须含渠道维度
  2. 重试不换键:失败重试时重新生成键 → 每条重试都"看起来是新消息" → 重试必须复用原幂等键
  3. DB 唯一索引冲突不处理:插入冲突直接报错 → 业务炸 → 冲突捕获后按"已存在"处理(幂等跳过)
  4. 渠道 API 返回"成功"但没发出去:渠道半成功(状态不确定)→ 用对账任务定期核对渠道回执

五、监控与指标

✅ ✅ ✅ ✅ ✅ 各 去 重 送 渠 渠 重 试 达 道 道 率 队 率 成 ( 列 ( S 功 被 深 对 L 率 幂 度 账 A ( 等 ( 后 ( 周 拦 积 : 哪 / 下 压 真 个 日 的 告 正 渠 维 比 警 到 道 度 例 ) 用 老 ) — 户 失 — 手 败 重 里 试 的 → 是 比 否 例 换 健 ) 供 康 应 ) 商 )

总结

消息推送防重的核心认知:

  1. 幂等键 = biz_type + biz_id + user_id + channel:确定性生成,含渠道维度
  2. DB 唯一索引是最后防线:代码漏了,DB 拦
  3. 失败要分可重试/不可重试:退避重试 + 告警 + 降级补偿
  4. 对账是"不遗漏"的保证:渠道回执核对,确认送达

推送系统的口碑 = 不打扰(不重)+ 不缺席(不漏)。 幂等防重 + 失败补偿 + 对账,三件套齐了才敢说"推送可靠"。