通知基础 - 重新认识通知

入职第一天

周一早上,我准时到达公司。前台小姐姐给我办了入职手续,领了工牌。

老张把我带到技术部,指着一张乱糟糟的桌子:“这是你的位置。”

桌子上有两台显示器,一把机械键盘。旁边堆着几本技术书:《深入理解计算机系统》、《数据密集型应用系统设计》。

“这些是…”

“前一个同事留下的,” 老张说,“他是个技术狂人,可惜…”

“可惜什么?”

“可惜他写的代码只有他自己能看懂。”

我苦笑。这种情况见多了。

查看现有系统

老王(技术负责人)走过来:“来,我给你介绍一下通知系统的现状。”

他打开代码库,给我看了核心代码:

我看完后,问了第一个问题:“这个脚本每分钟执行一次?”

“对,我们用 crontab 设置的。”

验证要点

  • 命令只用于验证系统状态,读者不需要记具体参数。

“那如果一分钟内发送不完 1000 条呢?”

老王愣了一下:“应该…能发完吧?”

我摇摇头:“如果推送服务响应慢,或者网络抖动,脚本执行时间可能超过一分钟。这时候 crontab 又会启动新的进程,两个进程同时处理,可能重复发送。”

“那怎么解决?”

“这是第一个问题,记下来吧。”

我打开笔记本,开始记录:

问题 1:并发执行导致重复推送

深入调查

接下来,我查看了数据库表结构:

数据设计要点

  • 核心是在 notifications 里保存业务事实,而不是把规则散落在应用逻辑里。
  • 索引服务于高频查询,重点是缩小扫描范围,而不是堆更多字段。

“这个表有什么问题?” 老王问。

我指出几个问题:

问题 2:查询效率低

数据设计要点

  • 查询目标是快速定位状态、任务或资源,避免在关键路径上做大范围扫描。

“这条 SQL 看起来没问题啊,用了索引…” 老王说。

“status 字段有索引吗?”

老王沉默了。

我执行了 EXPLAIN

+----+-------------+---------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table         | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+---------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | notifications | ALL  | NULL          | NULL | NULL    | NULL | 200000 | Using where |
+----+-------------+---------------+------+---------------+------+---------+------+--------+-------------+

“全表扫描!200 万条数据,每次扫描 200 万条,只为了找 1000 条待发送的。”

问题 3:表会无限增长

“每条通知都永久存储?数据量会越来越大。”

“用户可能需要查看历史通知…” 老王说。

“查看历史通知和待发送通知,是两个不同的需求,应该分开处理。”

问题 4:没有错误处理和重试机制

“如果推送失败怎么办?”

“状态设为 ‘failed’ 就不管了。” 老王说。

“那这些失败的通知就永远发不出去了?“

通知的本质

午饭时间,我一个人在工位上思考。

通知系统的核心是什么?

我想起之前读过的一篇论文,把通知系统拆解为三个阶段:

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   产生阶段   │────▶│   存储阶段   │────▶│   投递阶段   │
└──────────────┘     └──────────────┘     └──────────────┘
     业务事件           消息队列             推送服务
     触发通知           持久化存储           多渠道分发

产生阶段

  • 用户 A 评论了用户 B 的动态
  • 系统生成一条通知:“用户 A 评论了你的动态:…”

存储阶段

  • 通知持久化到数据库
  • 记录通知类型、内容、接收者、状态

投递阶段

  • 选择合适的推送渠道
  • 发送推送(APNs/FCM/厂商推送)
  • 更新状态

当前的系统把这三个阶段混在一起,耦合度太高。

用户旅程分析

下午,我找到产品经理小李,想了解用户对通知的期望。

“用户最关心什么?”

小李想了想:“第一时间收到通知,不想错过重要消息。但也不要被打扰,比如半夜不要发促销。”

“用户会怎么使用通知?”

她给我画了一个用户旅程:

用户打开 App

看到通知红点(手机桌面)

点击通知,进入 App

查看具体内容

可能会查看历史通知(消息中心)

“所以通知有两条路径?”

“对,” 小李说,“一条是即时推送,弹到用户手机上。另一条是站内信,用户打开 App 后能看到。”

“这两种通知有什么区别?”

“推送是瞬间的,用户可能错过。站内信是持久的,用户随时可以查看历史。”

我恍然大悟。通知不只是推送,还有站内信。这两个是互补的,不是替代关系。

分类通知

我回到工位,开始分析通知的类型:

即时性要求

  • 高:私信、评论@、关注
  • 中:点赞、系统公告
  • 低:促销、周报

用户关注度

  • 高:私信、评论@、关注
  • 中:点赞、系统公告
  • 低:促销、活动

发送频率

  • 高频:点赞(一个热门动态可能有上千个赞)
  • 中频:评论
  • 低频:系统公告

问题 5:不同类型通知混在一起处理

“点赞通知和私信通知,重要性完全不同,” 我在笔记本上写道,“不应该用同样的方式处理。“

初步方案

晚上,我整理了一下思路,写了一份改进方案:

短期目标(1 周)

  1. 解决重复推送

    • 使用文件锁或 Redis 分布式锁,防止并发执行
    • 记录推送结果,幂等处理
  2. 优化查询效率

    • 给 status 字段加索引
    • 考虑使用 Redis 队列代替数据库轮询
  3. 增加错误处理

    • 推送失败时记录原因
    • 实现重试机制(最多 3 次)

中期目标(1 个月)

  1. 拆分通知类型

    • 高优先级队列:私信、评论@
    • 低优先级队列:点赞、促销
  2. 实现站内信

    • 设计消息中心
    • 实现已读/未读状态
  3. 用户偏好设置

    • 允许用户关闭某类通知
    • 设置免打扰时间

长期目标(3 个月)

  1. 多渠道推送

    • iOS:APNs
    • 安卓:厂商推送
    • 短信、邮件(备选)
  2. 推送监控

    • 推送成功率
    • 平均延迟时间
    • 各渠道统计

老王看完方案,点点头:“思路清晰,先解决燃眉之急。“

第一步:加锁

第二天一上班,我就开始改造。

第一步,解决并发问题。我选择使用 Redis 分布式锁:

部署后,观察了一天,重复推送的问题消失了。

但我知道,这只是开始。

小结

入职第一天,我学到:

  1. 通知系统的三个阶段:产生、存储、投递
  2. 通知的两种形式:即时推送、站内信
  3. 通知的分类:按即时性、关注度、频率区分
  4. 核心问题:并发、效率、错误处理、分类

这只是第一步。接下来,我要解决更多问题:推送渠道、站内信设计、多端同步…

路还很长,但我已经看清了方向。