以 B 站为例,聊聊站内消息系统的设计

本文来自 guang19 投稿(Github 同名,欢迎关注)。前端

使用过简书,知乎或 b 站的小伙伴应该都有这样的使用体验:当有其余用户关注咱们或者私信咱们的行为时,咱们会收到相关的消息。 虽然这些功能看上去简单,但其背后的设计是很是复杂的,几乎是一个完成的系统,能够称之为 站内消息系统web

我以 b 站举例(我的认为 b 站的消息系统是我见过的很是完美的,UI 也最为人性化的):后端

b站站内消息

能够看到 b 站把消息大体分为了三类:微信

  1. 系统推送的通知(System Notice);
  2. 回复、@、点赞等用户行为产生的提醒(Remind);
  3. 用户之间的私信(Chat)。

这样设计不只分类明确,且处于同一个主体的事件提醒还会作一个聚合,极大的提升了用户体验,不让用户收到太多分散的消息。网络

举个例子:好比你在某个视频或某篇文章下发表了评论,有 100 我的给你的评论点了赞,那么你但愿消息页面呈现的是一个一个用户给你点赞的提醒,仍是像如下聚合以后的提醒:框架

消息的聚合

我相信你大几率会选择后者。编辑器

我认为对于不少应用来讲,这样的设计都是很是合理的,接下来我写写我对于消息系统的设计。ide

系统通知(System Notice)

系统通知通常是由后台管理员发出,而后指定某一类(全体,我的等)用户接收。基于此设想,能够把系统通知大体分为两张表:性能

  1. t_manager_system_notice(管理员系统通知表) :记录管理员发出的通知 ;
  2. t_user_system_notice(用户系统通知表) : 存储用户接受的通知。

t_manager_system_notice 结构以下:flex

字段名 类型 描述
system_notice_id LONG 系统通知 ID
title VARCHAR 标题
content TEXT 内容
type VARCHAR 发给哪些用户:单用户 single;全体用户 all,vip 用户,具体类型各位小伙伴能够根据本身的需求选择
state BOOLEAN 是否已被拉取过,若是已经拉取过,就无需再次拉取
recipient_id LONG 接受通知的用户的 ID,若是 type 为单用户,那么 recipient 为该用户的 ID;不然 recipient 为 0
manager_id LONG 发布通知的管理员 ID
publish_time TIMESTAMP 发布时间

t_user_system_notice 结构以下:

字段名 类型 描述
user_notice_id LONG 主键 ID
state BOOLEAN 是否已读
system_notice_id LONG 系统通知的 ID
recipient_id LONG 接受通知的用户的 ID
pull_time TIMESTAMP 拉取通知的时间

当管理员发布一条通知后,将通知插入 t_manager_system_notice 表中,而后系统定时的从 t_manager_system_notice 表中拉取通知,而后根据通知的 type 将通知插入 t_user_system_notice 表中。

若是通知的 type 是 single 的,那就只须要插入一条记录到 t_user_system_notice 中。若是是全体用户,那么就须要将一个通知批量根据不一样的用户 ID 插入到 t_user_system_notice 中,这个数据量就须要根据平台的用户量来计算。

举个例子: 管理员 A 发布了一个活动的通知,他须要将这个通知发布给全体用户,当拉取时间到来时,系统会将这一条通知取出。随后系统到用户表中查询选取全部用户的 ID,而后将这一条通知的信息根据全部用户的 ID,批量插入 t_user_system_notice 中。用户须要查看系统通知时,从 t_user_system_notice 表中查询就好了。

注意:

  1. 由于一次拉取的数据量可能很大,因此两次拉取的时间间隔能够设置的长一些。
  2. 拉取 t_manager_system_notice 表中的通知时,须要判断 state,若是已经拉取过,就不须要重复拉取, 不然会形成重复消费。
  3. 当一条通知须要发布给全体用户时,咱们应该考虑到用户的活跃度。由于若是有些用户长期不活跃, 咱们还将通知推送给他(她),这显然会形成空间的浪费。 因此在选取用户 ID 时,咱们能够将用户上次 登陆的时间与推送时间作一个比较,若是用户一年未登录或几个月未登陆,咱们就不选取其 ID,进而避免 无谓的推送。
  4. 有的小伙伴可能有疑问: 某条通知已经被拉取过的话,在其后注册的用户是否是不能再接收到这条通知? 是的。但若是你想将已拉取过的通知推送给那些后注册的用户,也不是特别大的问题。 只须要再写一个定时任务,这个 定时任务能够将通知的 push_time 与用户的注册时间比较一下,从新推送便可。

以上就是系统通知的设计了,接下来再看看较难的提醒类型的消息。

事件提醒(EventRemind)

之因此称提醒类型的消息为事件提醒,是由于此类消息均是经过用户的行为产生的,以下:

  • xxx 在某个评论中@了你;
  • xxx 点赞了你的文章;
  • xxx 点赞了你的评论;
  • xxx 回复了你的文章;
  • xxx 回复了你的评论。

诸如此类事件,咱们以单词 action 形容不一样的事件(点赞,回复,at)。 能够看到除了事件以外,咱们还须要了解用户是在哪一个地方产生的事件,以便当咱们收到提醒时, 点击这条消息就能够去到事件现场,从而加强用户体验,我以事件源 source 来形容事件发生的地方。

  • 当 action 为点赞,source 为文章时,我就知道:有用户点赞了个人某篇文章;
  • 当 action 为点赞,source 为评论时,我就知道:有用户点赞了个人某条评论;
  • 当 action 为@(at), source 为评论时,我就知道:有用户在某条评论里@了我;
  • 当 action 为回复,source 为文章时,我就知道:有用户回复了个人某篇文章;
  • 当 action 为回复,source 为评论时,我就知道:有用户回复了个人某条评论;

由此能够设计出事件提醒表 t_event_remind,其结构以下:

字段名 类型 描述
event_remind_id LONG 消息 ID
action VARCHAR 动做类型,如点赞、at(@)、回复等
source_id LONG 事件源 ID,如评论 ID、文章 ID 等
source_type VARCHAR 事件源类型:"Comment"、"Post"等
source_content VARCHAR 事件源的内容,好比回复的内容,回复的评论等等
url VARCHAR 事件所发生的地点连接 url
state BOOLEAN 是否已读
sender_id LONG 操做者的 ID,即谁关注了你,at 了你
recipient_id LONG 接受通知的用户的 ID
remind_time TIMESTAMP 提醒的时间

消息聚合

消息聚合只适用于事件提醒,以聚合以后的点赞消息来讲:

  • 100 人 {点赞} 了你的 {文章 ID = 1} :《A》;
  • 100 人 {点赞} 了你的 {文章 ID = 2} :《B》;
  • 100 人 {点赞} 了你的 {评论 ID = 3} :《C》;

聚合以后的消息明显有两个特征,即:action 和 source type,这是系统消息和私信都不具有的, 因此我我的认为事件提醒的设计要稍微比系统消息和私信复杂。

如何聚合?

稍稍观察下聚合的消息就能够发现:某一类的聚合消息之间是按照 source type 和 source id 来分组的, 所以咱们能够得出如下伪 SQL:

SELECT * FROM t_event_remind WHERE recipient_id = 用户ID
AND action = 点赞 AND state = FALSE GROUP BY source_id , source_type;

固然,SQL 层面的结果集处理仍是很麻烦的,因此个人想法先把用户全部的点赞消息先查出来, 而后在程序里面进行分组,这样会简单很多。

拓展

其实还有一种设计提醒表的作法,即按业务分类,不一样的提醒存入不一样的表,这样能够分为:

  1. 点赞提醒表
  2. 回复提醒表
  3. at(@)提醒表。

我认为这种设计比第一种的更松耦合,没必要全部类型的提醒都挤在一张表里,可是这也会带来表数量的膨胀。 因此各位小伙伴能够自行选择方案。

私信

站内私信通常都是点到点的,且要求是实时的,服务端能够采用 Netty 等高性能网络通讯框架完成请求。 咱们仍是以 b 站为例,看看它是怎么设计的:

站内消息系统的设计

b 站的私信部分能够分为两部分:

  1. 左边的与不一样用户的聊天室;
  2. 与当前正在对话的用户的对话框,显示了当前用户与目标用户的全部消息。

按照这个设计,咱们能够先设计出聊天室表 t_private_chat,由于是一对一,因此聊天室表会包含对话的两个用户的信息:

字段名 类型 描述
private_chat_id LONG 聊天室 ID
user1_id LONG 用户 1 的 ID
user2_id LONG 用户 2 的 ID
last_message VARCHAR 最后一条消息的内容

这里 user1_id 和 user2_id 表明两个用户的 ID,并没有特定的前后顺序。

接下来是私信表 t_private_message 了,私信天然和所属的聊天室有联系,且考虑到私信能够在记录中删除(删除了只是不显示记录,可是对方会有记录,撤回才是真正的删除),就还须要记录私信的状态,如下是个人设计:

字段名 类型 描述
private_message_id LONG 私信 ID
content TEXT 私信内容
state BOOLEAN 是否已读
sender_remove BOOLEAN 发送消息的人是否把这条消息从聊天记录中删除了
recipient_remove BOOLEAN 接受人是否把这条消息从聊天记录删除了
sender_id LONG 发送者 ID
recipient_id LONG 接受者 ID
send_time TIMESTAMP 发送时间

消息设置

消息设置通常都是针对提醒类型的消息的,且确定是由用户本身设置的。因此我想到通常有如下设置选项:

  1. 是否开启点赞提醒;
  2. 是否开启回复提醒;
  3. 是否开启@提醒;

下面是 b 站的消息设置:

消息设置

能够看到 b 站还添加了陌生人选项,也就是说若是给你发送私信的用户不是你关注的用户,那么视之为陌生人私信,就不接受。

如下是我对于消息设置的设计:

字段名 类型 描述
user_id LONG 用户 ID
like_message BOOLEAN 是否接收点赞消息
reply_message BOOLEAN 是否接收回复消息
at_message BOOLEAN 是否接收 at 消息
stranger_message BOOLEAN 是否接收陌生人的私信

总结

以上就是我对于整个站内消息系统的大概设计了,我参考了不少文章的内容以及不少网站的设计,但实际项目的需求确定与我所介绍的有不少出入,因此各位小伙伴能够酌情参考。

最后

文章有帮助能够点个「在看」或「分享」,都是支持,我都喜欢!

我是 Guide 哥,Java后端开发,会一点前端知识,喜欢烹饪,自由的少年。一个三观比主角还正的技术人。咱们下期再见!


往期推荐



来吧!手写一个 RPC 框架。毕设/项目经验稳了!

我在华为外包一年的经历分享。

我还在生产玩 JDK7,JDK 15 却要来了!|新特性尝鲜

朋友入职中软一个月(外包华为)就离职了!

线上频出MySQL死锁问题!分享一下本身教科书般的排查和分析过程!

6 个珍藏已久 IDEA 小技巧,这一波所有分享给你!


本文分享自微信公众号 - JavaGuide(JavaGuide)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索