找回密码
 会员注册
查看: 11|回复: 0

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

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
72245
发表于 2024-10-6 19:32:56 | 显示全部楼层 |阅读模式
本文来自 guang19 投稿(Github 同名,欢迎关注)。使用过简书,知乎或 b 站的小伙伴应该都有这样的使用体验:当有其他用户关注我们或者私信我们的行为时,我们会收到相关的消息。?虽然这些功能看上去简单,但其背后的设计是非常复杂的,几乎是一个完成的系统,可以称之为?站内消息系统。我以 b 站举例(个人认为 b 站的消息系统是我见过的非常完美的,UI 也最为人性化的):b站站内消息可以看到 b 站把消息大致分为了三类:系统推送的通知(System Notice);回复、@、点赞等用户行为产生的提醒(Remind);用户之间的私信(Chat)。这样设计不仅分类明确,且处于同一个主体的事件提醒还会做一个聚合,极大的提高了用户体验,不让用户收到太多分散的消息。举个例子:比如你在某个视频或某篇文章下发表了评论,有 100 个人给你的评论点了赞,那么你希望消息页面呈现的是一个一个用户给你点赞的提醒,还是像以下聚合之后的提醒:消息的聚合我相信你大概率会选择后者。我认为对于很多应用来说,这样的设计都是非常合理的,接下来我写写我对于消息系统的设计。系统通知(System Notice)系统通知一般是由后台管理员发出,然后指定某一类(全体,个人等)用户接收。基于此设想,可以把系统通知大致分为两张表:t_manager_system_notice(管理员系统通知表)?:记录管理员发出的通知 ;t_user_system_notice(用户系统通知表)?:?存储用户接受的通知。t_manager_system_notice 结构如下:字段名类型描述system_notice_idLONG系统通知 IDtitleVARCHARcontentTEXT内容typeVARCHAR发给哪些用户:单用户 single;全体用户 all,vip 用户,具体类型各位小伙伴可以根据自己的需求选择stateBOOLEAN是否已被拉取过,如果已经拉取过,就无需再次拉取recipient_idLONG接受通知的用户的 ID,如果 type 为单用户,那么 recipient 为该用户的 ID;否则 recipient 为 0manager_idLONG发布通知的管理员 IDpublish_timeTIMESTAMP发布时间t_user_system_notice 结构如下:字段名类型描述user_notice_idLONG主键 IDstateBOOLEAN是否已读system_notice_idLONG系统通知的 IDrecipient_idLONG接受通知的用户的 IDpull_timeTIMESTAMP拉取通知的时间当管理员发布一条通知后,将通知插入 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 表中查询就行了。注意:因为一次拉取的数据量可能很大,所以两次拉取的时间间隔可以设置的长一些。拉取 t_manager_system_notice 表中的通知时,需要判断 state,如果已经拉取过,就不需要重复拉取, 否则会造成重复消费。当一条通知需要发布给全体用户时,我们应该考虑到用户的活跃度。因为如果有些用户长期不活跃, 我们还将通知推送给他(她),这显然会造成空间的浪费。?所以在选取用户 ID 时,我们可以将用户上次 登录的时间与推送时间做一个比较,如果用户一年未登陆或几个月未登录,我们就不选取其 ID,进而避免 无谓的推送。有的小伙伴可能有疑问:?某条通知已经被拉取过的话,在其后注册的用户是不是不能再接收到这条通知??是的。但如果你想将已拉取过的通知推送给那些后注册的用户,也不是特别大的问题。?只需要再写一个定时任务,这个定时任务可以将通知的 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_idLONG消息 IDactionVARCHAR动作类型,如点赞、at(@)、回复等source_idLONG事件源 ID,如评论 ID、文章 ID 等source_typeVARCHAR事件源类型:"Comment"、"Post"等source_contentVARCHAR事件源的内容,比如回复的内容,回复的评论等等urlVARCHAR事件所发生的地点链接 urlstateBOOLEAN是否已读sender_idLONG操作者的 ID,即谁关注了你,at 了你recipient_idLONG接受通知的用户的 IDremind_timeTIMESTAMP提醒的时间消息聚合消息聚合只适用于事件提醒,以聚合之后的点赞消息来说: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?=?用户IDAND?action?=?点赞?AND?state?=?FALSE?GROUP?BY?source_id?,?source_type;当然,SQL 层面的结果集处理还是很麻烦的,所以我的想法先把用户所有的点赞消息先查出来, 然后在程序里面进行分组,这样会简单不少。拓展其实还有一种设计提醒表的做法,即按业务分类,不同的提醒存入不同的表,这样可以分为:点赞提醒表回复提醒表at(@)提醒表。我认为这种设计比第一种的更松耦合,不必所有类型的提醒都挤在一张表里,但是这也会带来表数量的膨胀。?所以各位小伙伴可以自行选择方案。私信站内私信一般都是点到点的,且要求是实时的,服务端可以采用 Netty 等高性能网络通信框架完成请求。?我们还是以 b 站为例,看看它是怎么设计的:站内消息系统的设计b 站的私信部分可以分为两部分:左边的与不同用户的聊天室;与当前正在对话的用户的对话框,显示了当前用户与目标用户的所有消息。按照这个设计,我们可以先设计出聊天室表 t_private_chat,因为是一对一,所以聊天室表会包含对话的两个用户的信息:字段名类型描述private_chat_idLONG聊天室 IDuser1_idLONG用户 1 的 IDuser2_idLONG用户 2 的 IDlast_messageVARCHAR最后一条消息的内容这里 user1_id 和 user2_id 代表两个用户的 ID,并无特定的先后顺序。接下来是私信表 t_private_message 了,私信自然和所属的聊天室有联系,且考虑到私信可以在记录中删除(删除了只是不显示记录,但是对方会有记录,撤回才是真正的删除),就还需要记录私信的状态,以下是我的设计:字段名类型描述private_message_idLONG私信 IDcontentTEXT私信内容stateBOOLEAN是否已读sender_removeBOOLEAN发送消息的人是否把这条消息从聊天记录中删除了recipient_removeBOOLEAN接受人是否把这条消息从聊天记录删除了sender_idLONG发送者 IDrecipient_idLONG接受者 IDsend_timeTIMESTAMP发送时间消息设置消息设置一般都是针对提醒类型的消息的,且肯定是由用户自己设置的。所以我想到一般有以下设置选项:是否开启点赞提醒;是否开启回复提醒;是否开启@提醒;下面是 b 站的消息设置:消息设置可以看到 b 站还添加了陌生人选项,也就是说如果给你发送私信的用户不是你关注的用户,那么视之为陌生人私信,就不接受。以下是我对于消息设置的设计:字段名类型描述user_idLONG用户 IDlike_messageBOOLEAN是否接收点赞消息reply_messageBOOLEAN是否接收回复消息at_messageBOOLEAN是否接收 at 消息stranger_messageBOOLEAN是否接收陌生人的私信总结以上就是我对于整个站内消息系统的大概设计了,我参考了很多文章的内容以及很多网站的设计,但实际项目的需求肯定与我所介绍的有很多出入,所以各位小伙伴可以酌情参考。最后文章有帮助可以点个「在看」或「分享」,都是支持,我都喜欢!我是 Guide 哥,Java后端开发,会一点前端知识,喜欢烹饪,自由的少年。一个三观比主角还正的技术人。我们下期再见!往期推荐来吧!手写一个 RPC 框架。毕设/项目经验稳了!我在华为外包一年的经历分享。我还在生产玩 JDK7,JDK 15 却要来了!|新特性尝鲜朋友入职中软一个月(外包华为)就离职了!线上频出MySQL死锁问题!分享一下自己教科书般的排查和分析过程!6 个珍藏已久 IDEA 小技巧,这一波全部分享给你!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 06:10 , Processed in 0.425299 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表