IM聊天功能设计

2023/03/01 SystemDesign 共 3231 字,约 10 分钟

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)。所以这也是写扩散的一个明显缺点,如果需要万人群就比较难了。

钉钉最开始使用的是写扩散模型,为了支持万人群,后来貌似优化成了读扩散。

Search

    Table of Contents