Skip to content

Commit b83eb89

Browse files
committed
增加:站内消息系统的设计完结
1 parent 78943e5 commit b83eb89

File tree

5 files changed

+315
-20
lines changed

5 files changed

+315
-20
lines changed

SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
## 架构设计
1515

1616
- [系统架构设计](gitbook_doc/system_architecture_design/系统架构设计简介.md)
17-
17+
18+
- [站内消息系统的设计](gitbook_doc/system_architecture_design/站内消息系统的设计.md)
1819

1920
---
2021

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# 站内消息系统的设计
2+
各位使用过简书,知乎或b站的同学应该都有这样的使用体验:当有其他用户关注我们或者私信我们的行为时,
3+
我们会收到相关的消息。 虽然这些功能看上去简单,但其背后的设计是非常复杂的,几乎是一个完成的系统,
4+
可以称之为站内消息系统。
5+
6+
我以b站举例(个人认为b站的消息系统是我见过的非常完美的,UI也最为人性化的):
7+
8+
![b站消息中心页面](../../img/站内消息系统的设计/b站消息中心页面.png)
9+
10+
可以看到b站把消息大致分为了三类:
11+
1. 系统推送的通知(System Notice);
12+
2. 回复,at,点赞等用户行为产生的提醒(Remind);
13+
3. 用户之间的私信(Chat)。
14+
15+
这样设计不仅分类明确,且处于同一个主体的事件提醒还会做一个聚合,极大的提高了用户体验,
16+
不让用户收到太多分散的消息。
17+
18+
举个例子:比如你在某个视频或某篇文章下发表了评论,有100个人给你的评论点了赞,
19+
那么你希望消息页面呈现的是一个一个用户给你点赞的提醒,还是像以下聚合之后的提醒:
20+
21+
![消息的聚合](../../img/站内消息系统的设计/消息的聚合.png)
22+
23+
我相信你大概率会选择后者。
24+
25+
我认为对于很多应用来说,这样的设计都是非常合理的,接下来我写写我对于消息系统的设计。
26+
27+
28+
## 系统通知(System Notice)
29+
系统通知一般是由后台管理员发出,然后指定某一类(全体,个人等)用户接收。
30+
基于此设想,可以把系统通知大致分为两张表:
31+
32+
1. 一张记录管理员发出的通知,称之为 t_manager_system_notice 管理员系统通知表;
33+
2. 一张存储用户接受的通知,称之为 t_user_system_notice 用户系统通知表。
34+
35+
t_manager_system_notice结构如下:
36+
37+
| 字段名 | 类型 | 描述 |
38+
| :---: | :---: | :---: |
39+
| system_notice_id | LONG | 系统通知ID |
40+
| title | VARCHAR | 标题 |
41+
| content | TEXT | 内容 |
42+
| type | VARCHAR | 发给哪些用户:单用户single;全体用户all,vip用户,具体类型各位同学可以根据自己的需求选择 |
43+
| state | BOOLEAN | 是否已被拉取过,如果已经拉取过,就无需再次拉取 |
44+
| recipient_id | LONG | 接受通知的用户的ID,如果type为单用户,那么recipient为该用户的ID;否则recipient为0 |
45+
| manager_id | LONG | 发布通知的管理员ID |
46+
| publish_time | TIMESTAMP | 发布时间 |
47+
48+
t_user_system_notice结构如下:
49+
50+
| 字段名 | 类型 | 描述 |
51+
| :---: | :---: | :---: |
52+
| user_notice_id | LONG | 主键ID |
53+
| state | BOOLEAN | 是否已读 |
54+
| system_notice_id | LONG | 系统通知的ID |
55+
| recipient_id | LONG | 接受通知的用户的ID |
56+
| pull_time | TIMESTAMP | 拉取通知的时间 |
57+
58+
**当管理员发布一条通知后,将通知插入t_manager_system_notice表中,然后系统定时的从
59+
t_manager_system_notice表中拉取通知,然后根据通知的type将通知插入t_user_system_notice表中。**
60+
如果通知的type是single的,那就只需要插入一条记录到t_user_system_notice中。如果是全体用户,
61+
那么就需要将一个通知批量根据不同的用户ID插入到t_user_system_notice中,
62+
这个数据量就需要根据平台的用户量来计算。
63+
64+
举个例子:
65+
管理员A发布了一个活动的通知,他需要将这个通知发布给全体用户,
66+
当拉取时间到来时,系统会将这一条通知取出。
67+
随后系统到用户表中查询选取所有用户的ID,
68+
然后将这一条通知的信息根据所有用户的ID,批量插入t_user_system_notice中。
69+
用户需要查看系统通知时,从t_user_system_notice表中查询就行了。
70+
71+
注意:
72+
1. 因为一次拉取的数据量可能很大,所以两次拉取的时间间隔可以设置的长一些。
73+
2. **拉取t_manager_system_notice表中的通知时,需要判断state,如果已经拉取过,就不需要重复拉取,
74+
否则会造成重复消费。**
75+
3. 当一条通知需要发布给全体用户时,我们应该考虑到用户的活跃度。因为如果有些用户长期不活跃,
76+
我们还将通知推送给他(她),这显然会造成空间的浪费。 所以在选取用户ID时,我们可以将用户上次
77+
登录的时间与推送时间做一个比较,如果用户一年未登陆或几个月未登录,我们就不选取其ID,进而避免
78+
无谓的推送。
79+
4. 有的同学可能有疑问: 某条通知已经被拉取过的话,在其后注册的用户是不是不能再接收到这条通知?
80+
是的。但如果你想将已拉取过的通知推送给那些后注册的用户,也不是特别大的问题。
81+
只需要再写一个定时任务,这个**定时任务可以将通知的push_time与用户的注册时间比较一下,重新推送**即可。
82+
83+
以上就是系统通知的设计了,接下来再看看较难的提醒类型的消息。
84+
85+
86+
## 事件提醒(EventRemind)
87+
之所以称提醒类型的消息为事件提醒,是因为此类消息均是通过用户的行为产生的,如下:
88+
- xxx 在某个评论中@了你;
89+
- xxx 点赞了你的文章;
90+
- xxx 点赞了你的评论;
91+
- xxx 回复了你的文章;
92+
- xxx 回复了你的评论。
93+
94+
诸如此类事件,我们以单词action形容不同的事件(点赞,回复,at)。
95+
可以看到除了事件之外,我们还需要了解用户是在哪个地方产生的事件,以便当我们收到提醒时,
96+
点击这条消息就可以去到事件现场,从而增强用户体验,我以事件源 source 来形容事件发生的地方。
97+
98+
- 当action为点赞,source为文章时,我就知道:有用户点赞了我的某篇文章;
99+
- 当action为点赞,source为评论时,我就知道:有用户点赞了我的某条评论;
100+
- 当action为at, source为评论时,我就知道:有用户在某条评论里at了我;
101+
- 当action为回复,source为文章时,我就知道:有用户回复了我的某篇文章;
102+
- 当action为回复,source为评论时,我就知道:有用户回复了我的某条评论;
103+
104+
由此可以设计出事件提醒表 t_event_remind,其结构如下:
105+
106+
| 字段名 | 类型 | 描述 |
107+
| :---: | :---: | :---: |
108+
| event_remind_id | LONG | 消息ID |
109+
| action | VARCHAR | 动作类型,如点赞、at、回复等 |
110+
| source_id | LONG | 事件源ID,如评论ID、文章ID等 |
111+
| source_type | VARCHAR | 事件源类型:"Comment"、"Post"等 |
112+
| source_content | VARCHAR | 事件源的内容,比如回复的内容,回复的评论等等 |
113+
| url | VARCHAR | 事件所发生的地点链接url |
114+
| state | BOOLEAN | 是否已读 |
115+
| sender_id | LONG | 操作者的ID,即谁关注了你,at了你 |
116+
| recipient_id | LONG | 接受通知的用户的ID |
117+
| remind_time | TIMESTAMP | 提醒的时间 |
118+
119+
120+
### 消息聚合
121+
消息聚合只适用于事件提醒,以聚合之后的点赞消息来说:
122+
123+
- 100人 {点赞} 了你的 {文章 ID = 1} :《A》;
124+
- 100人 {点赞} 了你的 {文章 ID = 2} :《B》;
125+
- 100人 {点赞} 了你的 {评论 ID = 3} :《C》;
126+
127+
聚合之后的消息明显有两个特征,即: action和source type,这是系统消息和私信都不具备的,
128+
所以我个人认为事件提醒的设计要稍微比系统消息和私信复杂。
129+
130+
131+
### 如何聚合?
132+
稍稍观察下聚合的消息就可以发现:某一类的聚合消息之间是按照source type 和 source id来分组的,
133+
因此我们可以得出以下伪SQL:
134+
135+
````sql
136+
SELECT * FROM t_event_remind WHERE recipient_id = 用户ID
137+
AND action = 点赞 AND state = FALSE GROUP BY source_id , source_type;
138+
````
139+
140+
当然,SQL层面的结果集处理还是很麻烦的,所以我的想法先把用户所有的点赞消息先查出来,
141+
然后在程序里面进行分组,这样会简单不少。
142+
143+
144+
### 拓展
145+
其实还有一种设计提醒表的做法,即按业务分类,不同的提醒存入不同的表,这样可以分为
146+
点赞提醒表,回复提醒表,at提醒表。 我认为这种设计比第一种的更松耦合,不必所有类型的
147+
提醒都挤在一张表里,但是这也会带来表数量的膨胀。 所以各位同学可以自行选择方案。
148+
149+
150+
## 私信
151+
站内私信一般都是点到点的,且要求是实时的,服务端可以采用Netty等高性能网络通信框架完成请求。
152+
我们还是以b站为例,看看它是怎么设计的:
153+
154+
![站内消息系统的设计](../../img/站内消息系统的设计/b站私信页面.png)
155+
156+
b站的私信部分可以分为两部分: 一部分是左边的与不同用户的聊天室;另一部分是与当前正在对话的用户的对话框,
157+
显示了当前用户与目标用户的所有消息。
158+
159+
按照这个设计,我们可以先设计出聊天室表 t_private_chat,因为是一对一,所以聊天室表会包含对话的两个用户的信息:
160+
161+
| 字段名 | 类型 | 描述 |
162+
| :---: | :---: | :---: |
163+
| private_chat_id | LONG | 聊天室ID |
164+
| user1_id | LONG | 用户1的ID |
165+
| user2_id | LONG | 用户2的ID |
166+
| last_message | VARCHAR | 最后一条消息的内容 |
167+
168+
这里user1_id和user2_id代表两个用户的ID,并无特定的先后顺序。
169+
170+
接下来是私信表 t_private_message 了,私信自然和所属的聊天室有联系,
171+
且考虑到私信可以在记录中删除(删除了只是不显示记录,但是对方会有记录,撤回才是真正的删除),
172+
就还需要记录私信的状态,以下是我的设计:
173+
174+
| 字段名 | 类型 | 描述 |
175+
| :---: | :---: | :---: |
176+
| private_message_id | LONG | 私信ID |
177+
| content | TEXT | 私信内容 |
178+
| state | BOOLEAN | 是否已读 |
179+
| sender_remove | BOOLEAN | 发送消息的人是否把这条消息从聊天记录中删除了 |
180+
| recipient_remove | BOOLEAN | 接受人是否把这条消息从聊天记录删除了 |
181+
| sender_id | LONG | 发送者ID |
182+
| recipient_id | LONG | 接受者ID |
183+
| send_time | TIMESTAMP | 发送时间 |
184+
185+
186+
## 消息设置
187+
消息设置一般都是针对提醒类型的消息的,且肯定是由用户自己设置的。所以我想到一般有以下设置选项:
188+
189+
1. 是否开启点赞提醒;
190+
2. 是否开启回复提醒;
191+
3. 是否开启@提醒;
192+
193+
下面是b站的消息设置:
194+
195+
![消息设置](../../img/站内消息系统的设计/消息设置.png)
196+
197+
可以看到b站还添加了陌生人选项,也就是说如果给你发送私信的用户不是你关注的用户,那么视之为陌生人私信,
198+
就不接受。
199+
200+
以下是我对于消息设置的设计:
201+
202+
| 字段名 | 类型 | 描述 |
203+
| :---: | :---: | :---: |
204+
| user_id | LONG | 用户ID |
205+
| like_message | BOOLEAN | 是否接收点赞消息 |
206+
| reply_message | BOOLEAN | 是否接收回复消息 |
207+
| at_message | BOOLEAN | 是否接收at消息 |
208+
| stranger_message | BOOLEAN | 是否接收陌生人的私信 |
209+
210+
211+
### 总结
212+
以上就是我对于整个站内消息系统的大概设计了,我参考了很多文章的内容以及很多网站的设计,但实际项目
213+
的需求肯定与我所介绍的有很多出入,所以各位同学可以酌情参考。
Loading
Loading

0 commit comments

Comments
 (0)