设计通知系统:多通道推送与频率控制

FreeGuideOnline 最新 2026-06-19

通知系统设计:多通道推送与频率控制

在现代应用系统中,通知是连接用户与信息的核心纽带。从社交互动到系统告警,高效的通知系统需要可靠送达、合理触达,并避免信息轰炸。本教程从零开始,带你设计一个支持多通道推送与智能频率控制的通知系统。

为什么需要专门设计通知系统

通知不仅仅是“发一条消息”这么简单。随着业务发展,你会面临以下挑战:

  • 通道多样化:站内信、短信、邮件、App Push、微信模板消息等,每个通道有独立的调用方式、限流和成本。
  • 触达策略复杂:同一用户可能希望不同消息在不同时间、不同通道上接收。
  • 防打扰:无控制的推送会造成用户流失,甚至被封禁通道权限。
  • 高可用与一致性:系统故障时消息不能丢失,且应尽量避免重复发送。

核心需求梳理

功能性需求

  • 支持多通道:App Push、短信、邮件、站内信、Webhook 等。
  • 优先级管理:验证码类高优,营销类低优。
  • 模板化消息:内容与通道解耦,支持变量替换。
  • 用户偏好设置:让用户选择接收通道和免打扰时段。
  • 发送记录与追踪:送达率、点击率、退订记录。

非功能性需求

  • 高可伸缩:大促期间可平滑支撑10倍量级。
  • 低延迟:验证码须在5秒内到达。
  • 高一致性:同一条通知不会重复发送给用户。
  • 容错隔离:单一通道故障不影响其他通道。

系统整体架构

我们采用典型的分层架构,将消息生产、路由、加工、投递与反馈处理解耦。

[业务服务] -> 消息队列(Kafka/RabbitMQ) -> 推送引擎
                                          |
                      +-------------------+-------------------+
                      |                   |                   |
                  路由网关           频控 / 去重         通道适配器
                      |                                     |
                用户偏好服务                         [短信/邮件/Push...]

消息队列:所有通知先入队,削峰填谷,保证业务接口快速响应。
推送引擎:核心消费组件,负责校验、频控、路由、渲染并投递。
通道适配器:用适配器模式封装第三方SDK,对上层屏蔽具体细节。

多通道推送设计

统一消息模型

所有通知进入系统后,使用一个标准化的消息体,避免每个通道各写一套逻辑。

{
  "messageId": "uuid",
  "userId": "用户ID",
  "bizType": "ORDER_SHIPPED",
  "priority": "HIGH",
  "channels": ["PUSH", "SMS"],
  "templateCode": "order_shipped_tpl",
  "params": {
    "order_no": "123456",
    "tracking_url": "https://..."
  },
  "callbackUrl": "https://callback.example.com",
  "createTime": 1717401600000
}

字段说明

  • bizType:业务类型,决定后续的频控规则和模板。
  • channels:期望送达的通道列表,由业务方指定,系统可能根据用户偏好裁剪。
  • templateCode:对应各通道的模板,如短信模板 ID、邮件模板名称。

通道路由与用户偏好

路由流程

  1. 解析消息的 channels 字段。
  2. 调用用户偏好服务,查询该用户对各通道的订阅状态、免打扰时段、手机号/邮箱的有效性。
  3. 交叉过滤:最终通道 = 请求通道 ∩ 用户已启用通道 ∩ 非免打扰通道
  4. 若结果为空,可降级到站内信或丢弃,并记录告警。

用户偏好服务 存储结构示例(Redis + DB):

Key: user:pref:10086
Value: {
  "push": {"enabled": true, "quietHours": ["22:00-07:00"]},
  "sms": {"enabled": true, "quietHours": []},
  "email": {"enabled": false}
}

通道适配器实现

采用策略模式为每个通道构建独立适配器,实现统一接口:

interface ChannelAdapter {
    SendResult send(Message msg, UserProfile user);
    boolean healthCheck();
}
// SMSSender, EmailSender, PushSender 各自实现

适配器负责:

  • 查询该用户的 channel 特有凭据(如设备Token、邮箱地址)。
  • 调用第三方 API(带上模板变量渲染后的内容)。
  • 处理返回结果,将状态回写至消息日志,失败时触发重试机制。

频率控制与去重

这是平衡用户体验与信息触达的关键模块,分单用户频控全局频控去重三部分。

单用户维度频控

防止向同一用户短时倾泻通知。设计两级滑动窗口:

频控类型 窗口 限制示例 用途
危急通知(验证码) 60秒 1次 防止通道费用浪费和用户骚扰
营销通知 1小时/天 3次/小时, 5次/天 保障营销效果又不致反感
系统通知 无强制限制 按用户维度动态调节 让用户自行设置上限

实现方式:使用 Redis 的 ZSET 滑动窗口记录发送时间戳。

伪代码逻辑:

def check_user_rate_limit(user_id, biz_type):
    key = f"notify:freq:{user_id}:{biz_type}"
    now = time.time()
    # 取窗口规则
    window_seconds, max_count = get_limit_config(biz_type)
    # 移除窗口外记录
    redis.zremrangebyscore(key, 0, now - window_seconds)
    # 计算当前计数
    current_count = redis.zcard(key)
    if current_count >= max_count:
        return False
    # 加入本次发送时间戳
    redis.zadd(key, {str(now): now})
    redis.expire(key, window_seconds + 10)
    return True

全局通道限流

每个通道有第三方限制(如短信每秒200条),在适配器层用令牌桶或信号量控制。

令牌桶示例(基于 Guava RateLimiter):

RateLimiter smsLimiter = RateLimiter.create(200.0); // 每秒200个令牌
sendResult = smsLimiter.acquire() ? smsAdapter.send(msg) : rejectWithRetry();

消息去重(Exactly-Once 语义)

网络重试、业务幂等缺失可能导致用户收到重复通知。我们通过发送记录表 + Redis 互斥锁保障。

  1. 发送记录表(MySQL):
    • 字段:message_id, user_id, channel, status, send_time
    • 联合唯一索引:(message_id, user_id, channel)
  2. Redis 缓存最近消息 ID
    • Key: sent:msg:<userId>:<channel>
    • Value: message_id,设置合理的过期时间(如7天)
  3. 处理流程
    • 消费到消息后,先 SET NX 尝试占位,成功则继续发送,失败则查询发送记录确认是否已发。
    • 若发送中第三方超时,记录 PENDING 状态,由定时任务补偿查询最终结果,防止双发。

模板化与内容渲染

各通道内容格式差异大,但核心业务参数不变。维护一套模板表:

模板编码 通道 内容模板 参数说明
order_shipped SMS 您的订单${order_no}已发货,点击查看:${short_url} order_no, short_url
order_shipped PUSH 标题:订单已发货 内容:您的订单${order_no}已由${carrier}发出 order_no, carrier

渲染时使用模板引擎(如 Thymeleaf、Freemarker)统一替换变量,并针对通道做内容截断、URL 短链转换。

可靠性保障

  • 消息持久化与重试
    • 消息队列开启持久化,消费失败重新入队或进入死信队列。
    • 发送失败指数退避重试(最多3次),最终失败记录到异常表,必要时人工介入。
  • 监控与告警
    • 各通道成功率、P99延迟、频控拦截率。
    • 死信队列积压、模板缺失等异常打点。
  • 灰度发布:新模板或新通道先对内部用户测试,稳定后全量放开。

实践总结

设计一个企业级通知系统,本质是让“正确的消息,通过合适的通道,在恰当的时间,触达愿意接收的用户”。核心在于抽象统一的消息模型、解耦通道适配、精细化的频控和优雅的容错机制。希望本教程为你构建自己的通知中心提供了清晰的路径。

如果想深入了解具体实现,可继续关注后续针对各厂 Push 通道适配、或基于 Flink 的实时频控方案专题。