消息通知系统的设计
站内信通知系统的设计
站内信系统是一个成熟的后端系统所应该具有的基本系统组件
需求分析
站内信通知系统的核心目标是为系统提供一个 用户与用户,系统与用户交互的手段,属于网站信息传播的一个重要途径,如果详细考虑,该系统实在是一个非常庞大的系统设计,可以做的事情非常的多
这里只是简单梳理一下普通消息系统需要做到的部分和功能设计,并提供一个可用的实际消息系统框架
一个完整的消息通知系统大概可以分为两部分, 消息系统和通知系统
消息系统主要负责消息的产生,接收等,通知系统则要实现事件机制,通知机制,对接各种消息通知平台(短信,微信,邮件等)
消息通知系统的核心处理大概可以分为以下3个部分:
消息产生
消息的产生:消息如何产生,来源和消息对象的结构
推送消息
消息的分发:消息如何到达用户,用户如何获取消息
处理消息
消息的处理:用户可以对消息所做的操作
同时还要在整个过程中随时持有消息的状态,这样才能最大化消息通知系统的功能
一个成熟消息处理的流程大概如下图:
消息产生
消息系统的消息按类型大致可以分为私信类和通知类,其中私信类就是上文提到的消息部分,又可以分为管理员发送的和用户个人发送的
私信类的消息大概情况如下:
- A给B发送了S内容,B给A回复了私信S2
- 管理员(admin)给A/B发送了S内容(这一种也可以看做是公告)
通知类,消息是由用户某些动作产生的提醒类的信息,具体情况大概如下(拿知乎举例子):
- A回答了问题W
- A在专栏Z中发布了文章P
- B评论了A在问题W下的回答H
- B赞了A在问题W下的回答H
- B赞了A在问题W中的回答H下的评论C
ps:用户: A,B,C 专栏: Z 回答: H 问题: W 文章: P 消息: S 评论: C
私信形式的实现起来比较容易,这里不多做表述,现在主要针对消息通知类型的进行方案分析
基于订阅模式的消息产生
消息类的通知总结一下就是A对B的某操作进行了某操作,具体模式是:
用户X 收到了 用户B 对 对象O 的 事件E 操作的通知
这种模式很符合订阅模型,以下几种消息都可以用订阅关系表示
- B订阅了问题W的回答事件ER
- B订阅了专栏Z的发表事件EP
- B订阅了回答H的评论
- B订阅了回答H的点赞事件
- B订阅了评论C的点赞事件
只要根据触发改消息的那条记录生成对应的消息即可
表结构设计
具体设计如下:
ps:(这里模拟了几条记录,方便用来做演示)
订阅关系表
用于记录用户的订阅信息
id | 用户 | 订阅对象id | 订阅对象类型 | 订阅事件 | 时间 |
---|---|---|---|---|---|
1 | B | 30 | post | answer | 2017-01-01 |
2 | B | 1 | zhuanlan | publish | 2016-01-01 |
3 | B | 112 | answer | common | 2016-01-01 |
4 | B | 113 | answer | up | 2018-01-01 |
5 | B | 12 | comment | up | 2018-01-01 |
当某对象产生某动作的时候,根据订阅关系表的订阅关系生成消息
订阅配置
用于为用户生成默认的订阅配置
id | 动作 | 订阅事件 |
---|---|---|
1 | 关注问题 | 问题更新/问题回答 |
2 | 回答 | 回答被评论/被点赞 |
这个表格记录了用户的某些操作会订阅怎样的对象动作,用于生成用户默认的订阅事件,后期如果开放权限,用户就可以对自己收到的提醒类型进行定制
消息内容表
消息内容表用来存储消息的具体内容,用户将来收到的信息就是该表中的信息
id | type | content |
---|---|---|
1 | notice | 用户C回答了问题W(id=30) |
2 | announce | 知乎形象刘看山发布了 |
3 | notice | B赞了你在问题W下面的R |
消息记录表
消息记录表用来存储消息和用户的分发关系
id | remindid(消息ID) | senderid(发送方) | reciverid(接收方) | isread | type |
---|---|---|---|---|---|
1 | 1 | 1 | 1 | 0 | message |
私信类消息
对于第一类私信类的消息,原则上即便用户不上线也需要对用户进行推送,也即是直接写入消息表
现在从消息的产生开始分析消息的数据流程
用户A. -> 发消息(“你吃饭了吗”) -> 消息内容表 -> 消息记录表
用户B上线 -> 查询消息记录表中的未读 -> 阅读消息 -> 回复内容
私信类型的比较简单,如果是管理员的话,把id设置为特殊值或者将消息类型标记为announce,在前台就可以进行相应的展示限制
提醒类消息
重点是第二类提醒类的消息,提醒类消息也不允许用户漏接
提醒类消息的产生流程就比较麻烦
某用户在某专栏发布某文章->生成消息存入消息表->检查订阅该专栏文章发布的用户和关注了该用户动态的其他用户->把消息表中的记录分发给这些用户->这些用户上线收到消息
你回答了一个问题->增加订阅该回答的点赞和评论动作->有人评论你的回答->生成消息内容表的内容->检查订阅该回答评论的用户->分发消息
例如以上记录会产生消息:
- 用户C在专栏Z(id=1)中发布了文章P
- 用户C评论了某用户在问题W下的回答H
- B赞了你在问题W下的回答(该消息推送给回答者)
消息表用于存储消息信息,当某事件被触发时,会生成对应的消息提醒内容,然后查询订阅该事件的所有用户,将消息和用户关系写入消息记录表
通知合并
有时候,当某用户收到大量用户对某对象进行相似的操作的时候为了性能和用户体验,我们需要对用户的同志进行合并
比如: 用户A 发布了一篇文章, 有5万人在1小时内都点赞了该文章, 我们就可以生成一条”张三,李四等5万个用户点赞了你的文章XXX”.
消息合并的规则:
- 按时间合并消息
- 按发送方合并消息
- 按种类合并消息
合并的周期:
- 固定时间的周期性的消息进行汇总
- 无固定时间,产生未读消息即汇总
合并的具体方法:
- C点赞了你的回答之后,这条消息会被标记为可聚合,聚合keyword为操作ID/对象类型/对象ID
例如: 在某段时间之类有两个用户赞了你的评论,这个时候可以使用 C,V等两个用户赞了你的评论C,当产生第一条通知的时候,消息表中有一条消息: C赞了你的回答,这个时候V赞了你的回答之后,两条记录可以合并成一条”c,v等两个人赞了你的回答”, 这个例子中的两条记录的操作类型(都是点赞)和操作对象(都是你的回答)相同
消息分发
通知消息产生以后我们只是有个要推送给用户的消息体,怎么把消息推送给用户也是一个很重要的部分
通知的分发
消息分发一半常用的有两种方式,一种是消息推送(push)一种是消息拉取(pull),不过现在大多采用两者结合的方式,针对不同的场景使用不同的方式
- push方式: 推送你有XX条消息未读(针对在线的用户)
- pull方式: 用户点击未读消息时对内容进行拉取
消息分发的话可以采用redis来作为中间桥梁,将未读消息的数量存入redis, uid:unread:10 用户有10条未读消息
当用户点击未读消息的标志的时候,从消息记录+消息内容表获取该用户的具体未读信息内容
这里可以做一些优化:
- 未读消息太多的话会每次取前20条
- 某些公用的消息比如公告和某文章发布的消息可以存入redis,使用类似messageid:content:xxxx.这样的内容存储消息内容,当所有订阅该类消息的用户获取消息的时候会先从redis中获取数据,取不到的才从数据库中查询
分发频率
- 实时分发
- 按小时分发
- 按周和天分发
分发管道 Web,微信,邮件,短信
用户对消息的处理
通知已读
每条消息都应该带有一个是否已读的状态,以防止对用户造成重复的打扰
一旦用户点击获取消息,打开消息详情或点击消息体的任意连接,就算作已读该消息,已读消息不做重复提醒
已读消息的排序: 用户有30条未读消息,点击列表已读20条,剩余10条未读消息怎么处理
解决方案: 获取消息未读消息列表时按时间顺序取前20条
通知内容的处理
点击链接: 点击链接之后进入到与该消息有关的详情页面
回复: 用户可以对私信进行回复
删除: 用户可以删除消息
ps: 不同终端消息状态应保持统一
redis中消息的存储
redis中不存储普通的用户消息,但是会存储系统公告和文章更新之类容易被复用的消息
消息:
msgid:xx:content:你订阅的XXX专栏更新了
uid:xx:unread:10
消息回收
消息处理还有其他一些需要系统处理的地方
- 用户对话消息的显示范围(可以根据时间),是否允许用户删除
- 用户拉黑名单是否自动删除会话消息
- 用户长时间未读取的系统消息自动回收的时间
- 长时间的未读消息的处理, 永久保留,二次推送(通过其他渠道)
其他
一个消息系统还涉及到其他的一些关键的地方
消息的离线计算和处理
消息系统数据量有可能非常大,如果一个用户有50万粉丝,该用户发一篇文章,理论上就要为50万用户产生消息.想要实时计算消息基本是不可行的, 所以应该有一套成熟的数据处理系统来支持消息系统
新消息到达时候的交互
用户获取消息以后的UI交互也是消息系统的一部分功能,也是需要考虑的一部分
- 声音提示
- 标题闪烁
- 未读信息浮动层
- 弹窗
防骚扰
- 增加屏蔽功能
- 设定接受消息的权限(例如:仅我关注的人可以给我发消息)
- 黑名单
用户拉回
- 长时间未处理消息的用户进行二次推送(通过短信和邮件等)