通知基础 - 重新认识通知
入职第一天
周一早上,我准时到达公司。前台小姐姐给我办了入职手续,领了工牌。
老张把我带到技术部,指着一张乱糟糟的桌子:“这是你的位置。”
桌子上有两台显示器,一把机械键盘。旁边堆着几本技术书:《深入理解计算机系统》、《数据密集型应用系统设计》。
“这些是…”
“前一个同事留下的,” 老张说,“他是个技术狂人,可惜…”
“可惜什么?”
“可惜他写的代码只有他自己能看懂。”
我苦笑。这种情况见多了。
查看现有系统
老王(技术负责人)走过来:“来,我给你介绍一下通知系统的现状。”
他打开代码库,给我看了核心代码:
我看完后,问了第一个问题:“这个脚本每分钟执行一次?”
“对,我们用 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 周)
解决重复推送
- 使用文件锁或 Redis 分布式锁,防止并发执行
- 记录推送结果,幂等处理
优化查询效率
- 给 status 字段加索引
- 考虑使用 Redis 队列代替数据库轮询
增加错误处理
- 推送失败时记录原因
- 实现重试机制(最多 3 次)
中期目标(1 个月)
拆分通知类型
- 高优先级队列:私信、评论@
- 低优先级队列:点赞、促销
实现站内信
- 设计消息中心
- 实现已读/未读状态
用户偏好设置
- 允许用户关闭某类通知
- 设置免打扰时间
长期目标(3 个月)
多渠道推送
- iOS:APNs
- 安卓:厂商推送
- 短信、邮件(备选)
推送监控
- 推送成功率
- 平均延迟时间
- 各渠道统计
老王看完方案,点点头:“思路清晰,先解决燃眉之急。“
第一步:加锁
第二天一上班,我就开始改造。
第一步,解决并发问题。我选择使用 Redis 分布式锁:
部署后,观察了一天,重复推送的问题消失了。
但我知道,这只是开始。
小结
入职第一天,我学到:
- 通知系统的三个阶段:产生、存储、投递
- 通知的两种形式:即时推送、站内信
- 通知的分类:按即时性、关注度、频率区分
- 核心问题:并发、效率、错误处理、分类
这只是第一步。接下来,我要解决更多问题:推送渠道、站内信设计、多端同步…
路还很长,但我已经看清了方向。