目录
0.基础概念
1.新鲜事–消息流的实现方式
2.用户系统(1)–缓存
2.用户系统(2)–好友关系
3.网站系统、API–翻页
3.短网址系统
4.数据库拆分和一致性哈希
5.分布式文件系统
6.分布式数据存储系统
问题:设计聊天系统
1.明确设计需求
- 聊天
- 群聊
- 多机登录
- 在线状态
以微信为例,月活跃用户可以达到10亿级别,日发送消息数量百亿级别
QPS = 10B / 86400 ~ 116K,峰值QPS = 116K * 3 ~ 347K
。
假设每条记录30byte的话,需要0.3T的存储空间。
2.设计服务
- Message Service 消息存取
- Realtime Service 消息实时推送
3.存储结构设计
Message Table
column | data type |
---|---|
id | int |
from_user_id | int |
to_user_id | int |
content | text |
created_at | timestamp |
这是最基本的一些数据内容,存在的问题是要查询用户A和B之间的对话,需要如下SQL
SELECT * FROM message
WHERE from_user_id=A AND to_user_id=B OR from_user_id=B AND to_user_id=A
ORDER BY created_at DESC;
这里的问题是WHERE查询条件太复杂,SQL的执行效率非常低,而且如果是群聊,这种数据结构没办法拓展。
Thread Table 链式消息
还是以微信为例,进入微信页面,看到的一个个对话就可以称为一个Thread,每个Thread中包含聊天内容(Message).
这时候表结构就变成了
Message Table
column | data type |
---|---|
id | int |
thread_id | int |
user_id | int |
content | text |
created_at | timestamp |
Thread Table
column | data type |
---|---|
id | int |
participant_user_ids | text(自定义存储用户的结构) |
created_at | timestamp |
updated_at | timestamp |
这时候查询某个Thread下的Message就变成了
SELECT * FROM message
WHERE thread_id=1234
ORDER BY created_at DESC;
Thread中的私有信息
Thread对于每个用户都会存在一些私有信息,比如是否免打扰,未读信息数量等等。
这个时候需要对Thread进行进一步的拆分。添加UserThread表用来存储User在Thread上的私有信息
Thread Table
column | data type |
---|---|
id | int |
last_message | text |
created_at | timestamp |
avatar | varchar |
UserThread Table
column | data type | comment |
---|---|---|
id | int | pk |
user_id | fk | |
thread_id | fk | |
unread_count | int | |
is_muted | bool | |
updated_at | timestamp | |
joined_at | timestamp |
如何查询Thread id
用户发消息的时候可能并不知道Thread id是什么。
针对群聊,可以在Thread Table中添加一个哈希值(participant_hash_code),计算按参与的user_id排序后计算哈希值
对于私聊,可以自定义格式比如private::user1::user2。
数据库选择
Message Table
数据量很大,不需要修改,很像项目中的日志一样,可以使用NoSQL存储。
例如row_key=thread_id, column_key=created_at, value=其他信息
Thread Table
如果使用SQL的话,需要对thread_id(查询某个对话的信息)和participant_hash_code(查询用户之间是否已经有thread存在了)添加索引,因为会有频繁的查询操作
如果使用NoSQL的话,对于需要建立多个索引的情况,可以分成两个表
UserThread Table
也可以使用NoSQL, row_key=user_id, column_key=updated_at, value=其他信息
一个可行的系统流程
- 用户A发送一条消息给用户B
- 服务器收到消息,查询A和B之间是否有对话记录(Thread),如果没有就创建一个
- 根据Thread id创建Message
- B每隔几秒访问一次服务器获取最新消息
- B收到消息
4.拓展系统
之前的方案中用户要间隔一段时间才能收到消息,体验很差,不实时
Socket
Socket技术可以让服务器主动向客户端推送数据,这时系统流程变成如下:
因为Socket是双向连接,如果Push Server宕机了,Client端是可以知道的,这个时候可以用切换Server或者用轮询的方式作为备用方案即可。
群聊功能
假设一个群有500个人,如果不做任何优化,每次都要给这500个人发消息。
但是实际应用中,并不会所有人都在线,在线可能很少,比如10个人。
Message Service并不会存储在线信息,仍然会尝试发送消息,消息到了Push Server发现大部分人都不在线,白白浪费了490次消息传递。
解决方案
在Message Service和Push Server之间添加一个Channel Service。
为每个聊天的Thread添加一个Channel信息
对于群聊,在线用户需要先订阅到对应的Channel上
- 用户上线时,会通过Message Service查询Push Server信息,这时服务器同步找到用户所属的Channel,并通知Channel Service完成订阅
- Channel Service就可以记录哪些频道有哪些用户还活着
- 如果用户下线了,由Push Server通知Channel Service移除该用户
Message Service收到用户发的消息,找到对应的Channel并传递消息,这样由原来的发500条消息就变成了发1条消息,
由Channel Service找到在线用户的Push Server并把消息发送出去。
多机登录
一般有2种场景
- 不允许同类型的设备同时登录,比如2个手机同时登录
- 允许不同类型的设备同时登录,比如支持手机,Web端和客户端同时登录
解决方案
在session中记录用户的客户端信息
当用户尝试用新的客户端登录的时候,比如手机端,先查询是否已经有其他手机处于登录状态
- 如果没有,创建新的session
- 如果有,将对应的session设置为过期或者删除,并发送通知让已登录的设备登出。
显示在线状态
不能使用socket的连接状态作为用户的在线状态,因为如果网络不稳定,可能会导致连接时断时续。而且在线状态对于实时性要求并没有那么高。
可以在服务器中通过数据库保存在线信息,如user, last_updated_at, clinet_info等等
一般由用户主动告知服务器我还在线,实现简单,而且可以顺带查询下好友的在线状态。
5.给y总打个广告
文章中一些内容比如Socket, 多端登录,包括后面会有的RPC,匹配系统,在acwing的后端工程课里面都有涉及,想深入了解的也可以报个课hh
rp++