即时通讯系统IM存储设计 之 基础表拆分 & 缓存策略的思考

本文主要围绕个人在设计IM中核心业务(信息收发)中的存储&缓存层的构建时的一些思考。苦逼大学生,才疏学浅,请多指教!

基础表拆分

事不宜迟,首先非常简单暴力的概括一下信息收发中表拆分的业务需求,无非就是:

  • 三关系表:user_to_user \ user_to_device \ user_to_session
  • 单消息表(每条消息所具有的信息):msg(userId, sessionId, msgId, seqId, content, type, is_delete)
  • 单消息状态表:msg_state(userId, msgId, type)
  1. 对于关系表:
  • 用户 & 用户:两用户间好友or黑名单等关系
  • 用户 & 设备:在此IM上,同一用户在any设备上登录。记录用户的登录设备信息。用于下发信息 & 消息漫游等业务
  • 用户 & 会话:用户是某个群聊 or 私聊的成员
  1. 消息表:记录每条信息的 发出者,发出群聊,消息的唯一标识符,序列号(保证单群聊内信息的时序性),具体内容,信令类型(正常信息 or 撤回等),是否撤回。
  1. 消息状态表:记录每个用户对某消息是否已读 等状态

注意:消息状态并非是消息本身的属性。此处状态是针对每个用户而言。譬如说A信息可能B用户已读,但是C用户未读。此时在消息状态表中存储的状态就是不一样的

综上表,我们设计出了一个应对IM中信息收发业务的表架构雏形。但是在规模较大的情况下完全不可行的,需引入缓存及其他优化措施,此处主要解释缓存相关设计。

缓存策略的初步设计

注:下方设备与客户端同义

离线后上线同步消息 (穿透 & 旁路模式

在用户下线之后,原下发消息会不断挤压。等待用户再次登录的时候拉取。此处使用redis中的zset进行存储,以sessionID为key。不同的会话内,以消息的序列号(seqID)为score,msgID为value。存储离线消息信息。

当用户该设备再次上线时,从redis中对应sessionID拉取对应的msgID的list,将其返回。

在允许多设备登录的情况下,某条信息可能已经下发到A设备中并且被用户已读了。但是由于B设备未登录,B设备对此消息的状态是未知的,会默认拉取离线队列,并且再次将其标为未读。造成了消息状态不一致的问题。为解决这一问题,只需要在取出msgID-list时,查询消息状态表即可。

但是此举会大大降低拉取的效率。也许可以在缓存层再设计消息状态的分布式缓存?但这种做法同样需要维护消息状态缓存的一致性

一种好的方法是利用旁路缓存思想。在消息的状态发生变更的时候,直接将缓存删除。而当消息状态查询请求到来时,先查缓存,miss则查DB且回写缓存,hit则返回缓存。这一策略牺牲了一定的查询效率,但是尽最大努力保证DB层和cache层消息状态的一致性

同时,随时间增大,zset中的缓存数据并不会收敛,会一直积压离线消息。此操作本身会对存储空间造成一定的压力。可对其进行定期清理,或实现多设备间消息同步,但也同时会对客户端上压力。所以需根据IM的具体业务场景,商讨其中的存储实现细节。

映射关系存储于内存中(只读缓存)

在上述表关系中,用户 & 会话 & 设备之间的映射关系是一大重点。映射关系决定了信息的下发对象,是IM运行的根本。并且他的修改请求频率较少(用户加入群聊,新设备登录,添加好友的频率 对比 发送消息的频率)。

但是同时,对这类映射关系的读需求很大。每当一条信息下发,服务器需要根据sessionID找到含有的userID,再根据userID找到各用户的deviceID。在这每一步的查询中若miss造成了缓存击穿,影响非常严重。

综上,可以得出此板块读多写少。于是我们可以全盘维护一个映射关系的缓存。同时基于flink等大数据组件,实现数据的流式更新。但是这也会对存储空间造成很大的压力。当然,可以改变架构使用图数据库存储,或改为session绑定,但需同时修改接入层的架构。

上方的设计理论上对消息下发进行大量的优化,但只是针对于一般情况下的运行作分析,仍存在很多问题。下方分为极端情况 & 异常状况进行分析:

先回顾一下信息下发的真实流程:假设有k个群聊,每个群聊中有n名用户,每个用户具有m台设备。那么在所有设备在线的情况下,一条上行消息对应发送(n * m) - 1条下行消息(除去发送方)。k个群聊单条消息对应k * ((n * m) - 1)条下行消息,这一数量级是非常恐怖的。

(渣图如下)

即时通讯系统IM存储设计 之 基础表拆分 & 缓存策略的思考_第1张图片

极端情况

  1. 若IM产品的设计需求人数限制大(千人级,万人级)。那么在此架构下,单sessionID对应的userID非常庞大,是发散的。随着消息的发送,不可避免的会造成大Key问题。
  2. 上点围绕用户多的情况展开分析。但若群聊非常活跃,即使用户个数非极端,也会造成很大压力。与大Key不同,会产生热Key。前者指存储空间上的不收敛,占用空间资源大;后者指对key的查询等请求非常频繁,占用缓存引擎(如redis)资源大。
  3. 若采用上方所说的旁路解决消息状态一致性问题,每当某个消息被单用户查看(消息状态改变)时,都会删除缓存。假设,多个用户在短时间内同时查看同一未读消息(短时间内多人更新同一消息状态)。会造成缓存频繁删除,多个查询消息的状态请求直接打在DB上,经典缓存击穿
  4. 无论是大Key还是热Key还是普通Key,在现阶段机制下。当消息下发,必须经由userId -> sessionId -> deviceId的路线。成为性能瓶颈。

异常状况

假设缓存引擎清一色为redis

  1. 大量依赖redis,运行过程中对redis访问频率非常高。在资源上,高频率的访问难免会占用相当一部分的系统资源。从而影响到了核心服务,核心逻辑的运行。在空间上,对内存的使用存在巨大成本。在安全上,不严格的忽略副本等机制,假如redis崩了,全崩了。
  2. 在用户拉取离线消息的时候,会造成大量的缓存更新(消息状态缓存 & zset离线消息队列),其中涉及redis和DB。如果这时候拉取这一业务本身出错,由于两个数据库本身异构,会造成信息存储的不一致。

至此,初步讨论了设计IM时的典型问题,之后将对现有架构作进一步完善,并改善现有问题。

你可能感兴趣的:(im设计,缓存,redis,系统架构,数据库架构)