【技术实践】IM 消息数据存储结构设计
1 背景
在移动互联网高速发展的时代,生活中IM类产品已经是我们离不开的应用了,像微信、钉钉等都是以IM为核心功能的社交产品。另外也有一些应用不是以IM为核心,但是也是其重要功能,比如在线游戏、电商直播等应用。
在IM庞大的体系中,消息系统无疑是最核心的,而消息系统中,最关键的部分是消息的分发和存储。
在以往传统消息系统中,对于在线的用户,消息会直接实时发送到在线的接收方,消息发送完成后,服务器端并不会对消息进行落地存储。对于离线的用户,服务器端会将消息存入到离线库,当用户登录后,从离线库中将离线消息拉走,然后服务器端将离线消息删除,这样的缺点是消息不持久化,导致消息无法支持消息漫游,降低了消息的可靠性。而在我们的消息系统中,服务器只要接收到了发送方发上来的消息,在转发给接收方的同时也会在离线数据库以及历史消息库中进行消息的落地存储,消息的落地也就支持了整体的消息漫游等相关功能。
2 离线消息和历史消息的区别
离线消息,就是用户在离线过程中收到的消息,这些消息大多是用户比较关心的消息,具有一定的时效性。我们的系统设计,离线消息默认只保存最近七天的消息。离线消息在用户登录后会全量的获取,然后客户端根据会话进行整体离线消息的展示。
历史消息,存储了用户所有的消息,包括发的消息以及接收的消息。在客户端获取历史消息时,是按照会话进行分页获取的。历史消息的存储时间我们系统设计默认为半年,当然这个是可配置的。
3 消息的发送以及存储的流程
融云整体的消息发送以及存储的流程如下图所示:
用户发送消息到服务器端后,首先会进入到消息系统中,消息系统会对消息进行分发以及存储。对于在线的接收方,会选择直接推送消息,但是遇到接收方不在线或者是消息推送失败的情况下,也会有另外的消息获取方式,接收方会主动向服务器拉取未收到的消息,但是接收方何时来服务器拉取消息以及从哪里拉取是未知的,所以消息存入到离线库的意义也就在这里。
消息系统存储离线的过程中,为了不影响整个系统的更为平稳,融云使用了消息队列,消息是异步存入到离线库中的。
在分发完消息后,消息服务会同步一份消息数据到历史消息服务中,历史消息服务会对消息进行落地存储。对于新的同步设备,会有消息漫游的需求,这也是历史消息的主要作用。在历史消息库中,客户端可以拉取任意会话的全量历史消息。
4 离线消息以及历史消息存储区别
上述的图中我们能清晰的看到,离线消息我们存储介质选用的是 Redis,历史消息我们选用的是HBase。为何选用不同的存储介质针对的是不同的业务场景和读写模式。下面我们重点介绍一下离线消息和历史消息存储的区别。
离线消息的存储模式是放大写,如下图所示,每个用户都有自己单独的收件箱和发件箱,收件箱存放需要向这个接收端同步的所有消息,发件箱里存的是发送端发送的所有消息。二人会话中的消息会产生两次写,发送者的发件箱以及接收端的收件箱。而在群的场景下,写入会被更加的放大,如果群里有N个人,那一条群消息就会被放大写N次。
放大写的优点是,接收端的逻辑会非常清晰简单,只需要从收件箱里读取一次即可,大大降低了同步消息所需的读的压力,但是缺点就是写入会被放大,特别是针对群这种场景。
历史消息的存储模式是放大读,因为历史消息中,每个会话都保存了整个会话的全量消息。在放大读这种模式下,每个会话的消息只保存一次。相比放大写的那种模式,写入次数大大降低,特别是针对群消息,只需要存一次即可。但是缺点是接收端接收消息非常的复杂和低效,因为这种模式客户端想拉取到所有消息就只能每个会话同步一次,读就会被放大,而且可能会产生很多次无效的读,因为有些会话可能根本没有新消息。
在IM这种应用场景下,通常会用到写扩散这种消息同步模型,一条消息产生一条,但是可能会被读多次,是典型的读多写少的场景。一个优化好的系统,必须从设计上平衡读写压力,避免读或者写任意一个维度达到天花板。当然写扩展这种模式也有其弊端,比如万人群,会导致一条消息,写入了一万次。综合来讲,我们需要根据自己的业务场景做相应设计选择,我们的系统是根据了离线和历史消息的不同场景选择了写扩散和读扩散的组合模式。
5 客户端拉取消息
离线消息的获取针对的是自己的整个离线消息,包括所有的会话。离线消息的获取是自上而下的方式,一次获取200条。在客户端拉取离线消息的信令中,需要带上当前客户端缓存的消息的最大时间戳,上面的图我们应该知道,离线消息我们存储的是一个线性结构,Server 会根据这个时间戳向下查找离线消息,重装或者新安装 App 时,客户端可以传0上来,Server 也会缓存客户端拉取到的最后一条消息的时间戳,然后根据业务场景,客户端类型等因素来决定从哪里开始拉取,如果没有拉取完 Server 会在拉取消息的应答中带相应的标记位,告诉客户端继续拉取,客户端循环拉取,直到所有离线消息拉完。
历史消息的获取针对的是单一会话,在拉取过程中需要带上来对方的 ID(如果是单聊的话就是对方的 UserID,如果是群,则是群组ID以及当前会话的最前面消息的时间戳,Server 会定位到这个人的这个会话然后一次获取20条,采用的是自下而上的方式,即从最后面往前翻。只要有消息,客户端可以一直向前翻,手动触发获取会话的历史消息。
6 总结
本篇文章主要讲了IM中消息系统的消息分发、存储等,重点介绍了离线消息和历史消息的区别以及两者存储中所选用的不同存储方式以及其优缺点。关于文中内容,也欢迎大家随时留言与我讨论。