通知是微型博客的「唤醒系统」——它把「发生在你身上的事」主动送到用户眼前。但通知系统最大的挑战不是「发出去」,而是「发得对、发得巧、不重复、不丢」。
本文系统讲解:通知类型建模、聚合与去重、拉取/推送架构、多通道下发、已读状态与多端同步。
一、通知类型建模
1.1 常见通知类型
| 类型 | 触发事件 | 聚合性 |
|---|---|---|
| 点赞 | 有人赞你的帖 | 可聚合 |
| 转发 | 有人转你的帖 | 可聚合 |
| 回复 | 有人回复你的帖/评论 | 可聚合 |
| @提及 | 有人 @ 你 | 弱聚合 |
| 关注 | 有人关注你 | 可聚合 |
| 系统 | 平台消息 | 不聚合 |
1.2 通知数据模型
CREATE TABLE notifications (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL, -- 接收者
type SMALLINT NOT NULL, -- 1赞 2转 3评 4@ 5关注 6系统
actor_id BIGINT, -- 最近一个触发者
actor_ids BIGINT[], -- 聚合的全部触发者
post_id BIGINT, -- 关联对象
comment_id BIGINT,
agg_count INT DEFAULT 1, -- 聚合条数
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_notif_user ON notifications(user_id, is_read, created_at DESC);
1.3 通知入口
通知产生链路:
事件(点赞/回复等)→ 事件总线 → 通知服务
→ 判断接收者是否在线/在设置中开启 → 生成通知 → 聚合/入队
→ 拉取通道入库 + 推送通道下发
一句话总结:通知建模的关键是「类型可扩展、聚合有字段支撑」——用 type 区分、actor_ids 支撑聚合、agg_count 记录折叠数。
二、聚合与去重
2.1 为什么需要聚合
不聚合的后果:张三、李四、王五 5 秒内各赞一次 → 用户收到 3 条通知,烦人且淹没真实信息。
聚合:同对象、同类型的多次互动合并成一条「张三等 5 人赞了你」。
2.2 聚合规则
聚合键 = (user_id, type, post_id)
时间窗 = 同键下 N 分钟内(如 30 分钟)新增 → 并入同一条
超出时间窗 → 新开一条
实现:
通知表 UNIQUE 约束(user_id, type, post_id, window_start)
新事件 → INSERT ON CONFLICT 更新 agg_count + actor_ids
2.3 去重
- 幂等事件:同一事件重复投递(如消息队列重试)
用事件唯一键(如 event_id)去重
- 用户自身操作:自己赞自己 → 不通知
- 已拉黑用户:不通知
2.4 聚合示例 SQL
-- 聚合写入(简化)
INSERT INTO notifications (user_id, type, actor_ids, post_id, agg_count)
VALUES (:uid, 1, ARRAY[:actor], :post, 1)
ON CONFLICT (user_id, type, post_id, window_start)
DO UPDATE SET
actor_ids = notifications.actor_ids || EXCLUDED.actor_ids,
agg_count = notifications.agg_count + 1,
updated_at = now();
一句话总结:聚合把「N 条骚扰」折叠成「1 条信息」,用冲突更新原子完成并入——这是通知体验的核心细节。
三、拉取式与推送式
3.1 拉取式(Pull)
客户端轮询 / 刷新时拉取:
GET /api/notifications?cursor=xxx
优点:实现简单、无长连接
缺点:不实时、有轮询开销
适用:Web 端、低实时要求
3.2 推送式(Push)
服务端主动下发:
- WebSocket / SSE(在线端,实时)
- APNs / FCM(移动端,离线也到)
- 站内信(App 内红点)
优点:实时
缺点:通道维护复杂
3.3 双通道设计
在线:WebSocket 推送 → 即时更新红点/列表
离线:APNs/FCM 推送 → 系统通知栏
兜底:下拉刷新拉取 → 保证不丢
顺序:WS 优先 → 失败/离线走 APNs/FCM → 客户端刷新兜底
一句话总结:通知下发是「多通道冗余」——实时走长连接、离线走厂商通道、兜底靠拉取,三层保证「要么即时看到、要么下次打开能拉回」。
四、多通道下发架构
4.1 架构图
事件流 → 通知服务
├─ 入库(PostgreSQL notifications 表)
├─ 在线推送(WebSocket 网关 / SSE)
└─ 离线推送(APNs / FCM,经推送网关)
移动端:
App 打开时:建立 WS → 实时
App 关闭时:FCM/APNs → 通知栏
App 打开但 WS 断:拉取兜底
4.2 推送通道管理
- 设备 token 注册表(user → device tokens)
- token 失效清理(APNs/FCM 返回 Unregistered)
- 推送频率限制(防骚扰)
- 静默通知 / 声音 / 横幅的偏好设置
4.3 WebSocket 消息
{
"type": "notification",
"data": {
"id": 12345,
"type": 1,
"agg_count": 5,
"summary": "张三等 5 人赞了你",
"post_id": 888
}
}
一句话总结:多通道下发 = 「入库保证可靠 + WS 保证在线实时 + 厂商通道保证离线触达」三件套,核心是设备注册表与 token 生命周期管理。
五、已读状态与红点
5.1 已读/未读
- 未读计数:SELECT count(*) FROM notifications WHERE user_id=? AND is_read=false
- 全部已读:UPDATE notifications SET is_read=true WHERE user_id=?
- 单条已读:UPDATE ... WHERE id=?
5.2 红点缓存
Redis:unread:{uid} → 未读数(读时 INCR,清时 SET 0)
- 新通知 → INCR
- 已读 → 客户端拉取真实计数或 SET 0
- 保持「读后清零」与「服务端计数」最终一致
5.3 会话维度
通知可挂在「会话/私信」维度:
- 未读私信数、未读群消息数
- 与系统通知分开计数
- 红点分 Tab 展示(互动/私信/系统)
一句话总结:已读/红点的核心是「缓存计数 + 最终一致」——未读数放 Redis 扛高频读,读后清零靠客户端回传与服务端校验收敛。
六、多端同步与幂等
6.1 多端同步
用户同时登录手机 + 桌面:
同一通知在两个端都要显示、任一端已读则全局已读
方案:
- 通知 id 全局唯一 → 各端拉取同一条
- 已读状态存服务端 → 任一端读后同步
- WS 推送向用户的所有在线连接广播
6.2 幂等
- 通知生成幂等:同一事件不重复生成通知
(消息队列 at-least-once → 需事件唯一键去重)
- 已读幂等:重复已读请求无害
- 推送幂等:同一通知可能从多个通道到达 → 客户端去重(按通知 id)
6.3 可靠性保障
- 通知表是「真相源」,推送只是加速展示
- 丢失推送 → 下次拉取总能补回(拉取兜底)
- 推送失败 → 记录日志 + 重试 + 降级为拉取
一句话总结:多端同步的关键是「服务端状态 + 客户端幂等」——已读、通知 id、推送都归一,任一端的操作都能全局收敛。
七、总结
微型博客通知系统的建设路径可以概括为:先建模、再聚合、双通道、终同步。通知类型用枚举建模、actor_ids 支撑聚合折叠;聚合规则把骚扰变成信息;拉取与推送双通道覆盖在线与离线;已读红点用缓存计数保高频;多端同步靠服务端状态与幂等收敛。
通知系统是微型博客的「神经末梢」——它不生产内容,却决定内容能否抵达用户。发得准、发得巧、不重复、不丢失,这四件事做对,通知系统就从「打扰」变成「价值」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。