IM聊天功能设计
参考
https://www.cnblogs.com/theseventhson/p/16625031.html
https://xie.infoq.cn/article/19e95a78e2f5389588debfb1c
Model
会话: Thread. 这个会话头像是什么,什么时候创建的。成员都有谁。
消息: Message. 属于哪个会话。
(1)Thread Table:当然是列举了所有会话!注意,这里包括所有用户的所有会话。以微信为例,假如有30亿注册用户,平均每个用户有100个会话,那么这个表就有3000亿行!
- id:就是会话id啦,全局唯一!
- last_message: 这个会话最后收到的消息,是需要显实在app上的
- create_at: 会话的创建时间
- participant_user_ids: 会话的参与者。如果是两个人点对点聊天,就是另一个userId;如果是群聊,就需要把所有的user_id都列举了!可以做成【uid1,uid2,uid3…】的形式
- participant_user_hashcode: 对上面的participant_user_ids求hash,便于快速定位群聊消息的thread_id!
(2)UserThreadTable:
- user_id: 这个表存放的是特定用户的thread信息了
- 其他都是该user自己的属性
- 为什么要和ThreadTable分开了?松耦合可以防止数据冗余、臃肿,也利于后期表结构更改!当然确定也明显:需要join,增加耗时!
(3)Message Table:
- thread_id: 哪个会话的message?
- user_id: 哪个用户发的?
- content和create_at最重要了,懂得都懂!
存储选型
数据表的设计定了,数据该用什么存了? 以2019年其公开课提供的10.8亿月活【现在肯定不止这个数了】、日发送450亿条message推算:
- Average QPS = 45B / 86400 = 520k,每秒发送52w条message
- Peak QPS = 520k * 3 = 1.5m,峰值按照3倍计算,每秒发送150w条message
- 假设每条message占用50byte空间,每天45billion的message大约需要2T的空间!
这么大的数据量,这么高的并发量,是用sql,还是nosql了?
(4)先来分析一下MessageTable,有以下两个特点:
- 读多,写多,一条message就像一条log日志
- 修改少
需要用timestamp对消息的收发顺序排序
综合对比,还是用nosql数据库吧!以Cassandra为例,存储结构如下:
- row_key = thread_id
- column_key = created_at 因为要按照时间倒序
- value = 其他信息,比如表的其他字段
可以直接用thread_id做index,用creat_at来sort message!sharding直接用thread_id就好!
(5)UserThread Table:这个表和user相关的,存放每个user自己private的数据,特点如下:
- 更新比较频繁
- 到了后期新增数据量会减缓
- 需要用timestamp对消息的收发顺序排序
综合考虑还是用nosql数据库,存储结构如下:
- row_key = user_id
- column_key = updated_at 按照更新时间sort
- value = 其他信息,比如表的其他字段
- 用user_id来sharding
(6)最后看看ThreadTable,特点如下:
- last_message会频繁更新
- 后期新增行的频率会基本稳定
这么来看,貌似用sql和Nosql都还行!如果用sql数据库,需要同时index by:
- thread_id:用于查询某个对话的信息
- participant_user_hashcode: 用户查询某两个或多个用户【就是群聊啦】之间是否已经有会话
如果用Nosql数据库了?如果同时支持按照 thread_id 和participant_hash_code 进行查询,我们需要两张表:因为nosql不支持join,所以需要根据participant_hash_code查thread_id【像不像ES的invert index?】,然后根据thread_id查last_message、avarta、create_at、particitant_user_ids等其他字段;相比于sql数据库的B+索引,速度快很多【sql数据库能做的,nosql数据库也能做,大不了就多建几张表嘛!其实sql建索引也是要额外耗费存储的】!两张表存储建二狗设计如下:
• 表1:Thread Table
• row_key = thread_id
• column_key = null
• value = 其他的基本信息
• 表2:ParticipantHashCode Table
• row_key = participant_hash_code
• column_key = null
• value = thread_id
总结
- Nosql存储。
- 基本上是按照thread id进行partition,然后时间有序。
优化
- 使用websocket,心跳保持在线
- 对于群聊,只给在线的user push message就行了。 增加一个Channel Service(频道服务),为每个聊天的Thread增加一个Channel信息。 对于较大群,在线用户先需要订阅到对应的 Channel 上。
- 群发消息时,先发到群聊thread上,然后再让channel push。松耦合设计。
读扩散和写扩散
读扩散的优点:
- 1)写操作(发消息)很轻量,不管是单聊还是群聊,只需要往相应的信箱写一次就好了;每个会话是一个信箱。
- 2)每一个信箱天然就是两个人的聊天记录,可以方便查看聊天记录跟进行聊天记录的搜索。
读扩散的缺点:读操作(读消息)很重,在复杂业务下,一条读扩散消息源需要复杂的逻辑才能扩散成目标消息。
在写扩散中,每个人都只从自己的信箱里读取消息。
但写(发消息)的时候,对于单聊跟群聊处理如下:
- 1)单聊:往自己的信箱跟对方的信箱都写一份消息,同时,如果需要查看两个人的聊天历史记录的话还需要再写一份(当然,如果从个人信箱也能回溯出两个人的所有聊天记录,但这样效率会很低);
- 2)群聊:需要往所有的群成员的信箱都写一份消息,同时,如果需要查看群的聊天历史记录的话还需要再写一份。可以看出,写扩散对于群聊来说大大地放大了写操作。
写扩散的消息同步模式,需要有一个额外的Timeline来专门用于消息同步,通常是每个接收端都会拥有一个独立的同步Timeline,用于存放需要向这个接收端同步的所有消息。
每个会话中的消息,会产生多次写,除了写入用于消息存储的会话Timeline,还需要写入需要同步到的接收端的同步Timeline。在个人与个人的会话中,消息会被额外写两次,除了写入这个会话的存储Timeline,还需要写入参与这个会话的两个接收者的同步Timeline。而在群这个场景下,写入会被更加的放大,如果这个群拥有N个参与者,那每条消息都需要额外的写N次。
写扩散优点: 写扩散同步模式的优点是,在接收端消息同步逻辑会非常简单,只需要从其同步Timeline中读取一次即可,大大降低了消息同步所需的读的压力。其缺点就是消息写入会被放大,特别是针对群这种场景。
- 1)读操作很轻量;
- 2)可以很方便地做消息的多终端同步。
写扩散缺点:写操作很重,尤其是对于群聊来说(因为如果群成员很多的话,1条消息源要扩散写成“成员数-1”条目标消息,这是很恐怖的)。
业界方案
微信采用的主要是:写扩散 + 推拉结合。由于群聊使用的也是写扩散,而写扩散很消耗资源,因此微信群有人数上限(目前是500)。所以这也是写扩散的一个明显缺点,如果需要万人群就比较难了。
钉钉最开始使用的是写扩散模型,为了支持万人群,后来貌似优化成了读扩散。