亚马逊AWS官方博客

骏书千里:社交实时聊天消息在云上的数据库设计

社交聊天已经广泛应用于我们的生活中。随着生成式 AI 发展,更多场景出现,例如虚拟交友、金融社区。这些场景中需要实时通信和即时消息,有些专业做通信的厂商,把通信聊天功能封装到 SDK,提供给游戏、电商、社交公司,只要在 App 嵌入 SDK 即可使用。如果自己构建整个聊天系统,需要更多工作。在用户量发展到很大规模时,如何构建稳定而快速的架构,采用何种数据库,成为需要思考的重要问题。

实时聊天消息的总体架构如下:

用户先登录到 API 服务器,通过状态服务器更新状态信息,并让用户连接到聊天服务器,可以是长连接,包括 web socket 交互。发送消息时,聊天服务器处理信息,根据接收消息用户的在离线状态进行处理,并存储到 Key Value 数据库。通知服务器推送消息,在线和离线用户都可以收到通知。

这是最简单的逻辑架构。真正实现,还会根据具体业务,加入更多组件,例如消息队列处理,数据分层存储等。

以下说明了消息队列在聊天消息场景的架构。消息存储库用于保存会话信息,而消息同步库用于多个客户端数据同步。消息进入队列后,进行处理,持久化存储到消息存储库,以供读取消息访问。此外,通常以写扩散方式,写入消息到同步库,各个接收者的收件箱,用户各自读取即可获得消息数据。现代即时通信,通常消息只会产生一次,但是会被读取多次,使用写扩散模式,效率更高。

业务场景中,还会细分单聊和群聊。单聊消息向自己和对方的信箱都写数据,群聊消息向群组信箱都写数据,聊天历史记录可以单独再写。消息漫游也是必要功能,可以在多个设备之间同步消息。按照消息接收者 ID,以 SortedSet 存储离线消息,按照时间先后顺序排列。消息 ID 要保证唯一,可以考虑 Snowflake 递增算法,高效产生自增排序 ID。

无论哪种场景,在用户量大的情况下,都会使用消息队列,起到削峰填谷作用。也会根据用户在线状态,处理消息。消息类似以下 key 格式:

msg_id, group_id, send_uid, recv_uid, recv_time, msg_body

每个 key,都会有对应的 value,适合 key value 的数据库,并且业务中经常需要按照时间列出最近的消息,需要有排序类型。Redis 正是此场景的最佳伴侣,好处在于:

  • 多种格式支持:包括 string, hash, sort set。
  • 高性能写入:写扩散模型要求数据库在低延迟的同时,保持高吞吐量写入。
  • 低延迟读取:即时通信要求在线消息实时送达并读取,需要毫秒或更低的读取延迟。

对于图片、视频、声音等大对象,通常会存储在更便宜的对象存储。Redis 存储对象元数据,应用需要访问时,通过元数据地址获取对象。

在数据量不大的时候,Redis 用于实时聊天非常合适。但是,当业务日益增长时,Redis 的昂贵内存成本成为重要的影响因素。并且,Redis 定位是缓存,即使有主从复制和 AOF 等机制,仍然存在丢失数据的风险。那么,如何高效并持久化保存消息数据?

有些开源的兼容 Redis 数据库支持硬盘存储,以更便宜的方式来存储 Redis 格式数据。但是此类数据库通常需要自己维护,磁盘性能不如原生内存 Redis,在数据量大的时候,多节点集群管理相对麻烦。

数据分层是海量数据的时候可以考虑的方案。真正的实时聊天中,客户端作为重要的缓存,可以加速客户访问历史数据。实时聊天产生的在线数据可以放在最热的数据层,例如 Redis,提供最高的性能。经过一段时间之后,需要持久化的温数据,可以放到专门的持久化数据库,例如 NoSQL 数据库(Amazon DynamoDB, Amazon DocumentDB)或者 SQL(Amazon RDS)。此后如果数据很久不访问,但是又需要保留,可以放到冷存储备份,例如 Amazon S3。

数据在不同分层的存储,可以制定规则移动数据。但是,移动数据本身也带来额外工作。有没有更简单的方式?

Amazon MemoryDB 兼容 Redis,通过额外的事务日志来保持数据持久化,在实现内存高性能 Redis 的同时,保证数据不会丢失。这样,通过一个数据库,就可以实现 Redis 聊天消息的持久化保存。

Amazon MemoryDB 是内存数据库,内存本身价格比较贵。如果很关注成本,也可以考虑同为 Key Value 格式的数据库Amazon DynamoDB。在数据持久化的基础上,即使在海量数据和高读写请求的情况下,仍然能保证稳定的毫秒级访问延迟。对于实时消息聊天场景,几毫秒和零点几毫秒的差别,客户体验差距并不明显。

以下是聊天消息的数据分层架构:

在此分层架构中,如果只需要一个数据库解决聊天消息存储的问题,兼顾持久化和高性能,可以选择 Amazon MemoryDB, Amazon DynamoDB 或者 Amazon DocumentDB(兼容 MongoDB),客户可以根据自身成本和开发情况进行选择。

整体实时聊天消息的云上参考架构如下,包括了负载均衡,API、管理、消息、状态、推送等服务,用户管理使用 Amazon RDS 关系型数据库,聊天消息使用 Amazon DynamoDB,实现高性能持久化存储。

Amazon DynamoDB 的表设计依赖于业务,按照业务需求,设置合适的主键,包括分区键和排序键,结合重要的业务查询维度建立二级索引。

聊天场景中,最重要的两个维度,是聊天会话和用户。有两种设计思路:

  • 以用户为中心

每个用户有自己的信箱,发送消息时,向自己和接收者的信箱写入消息。读取时读取自身信箱消息。如果每个用户的信箱单独建表,成本太大。可以创建用户信息表,包含所有用户的消息。用户为分区键,时间戳为排序键,存储消息id。此外创建单独的信箱消息表,对应具体的消息内容,以及发送者,接收者,时间戳,状态等信息。

  • 以会话为中心

记录会话信息,包括 chat id,时间,消息,消息状态,发送者,接收者,会话状态+时间。其中,会话作为业务中心维度,设置为主键。每个会话中有消息产生时,有时间戳,而会话通常会按照时间排序,取出最近的消息,时间戳适合作为排序键。其他关键属性包含发送者和接收者,消息,会话状态。业务通常还需要根据用户读取消息,或者搜索消息以及会话内容,可以创建相应的二级索引,以加速查询。

业务关联数据库实体关系(ERD)如下:

根据业务逻辑,创建表如下:

会话 chatid(分区键)和时间戳 ts(排序键)构成表的主键。用户量大的情况下,会话总是分散的,结合不同的时间戳,就可以记录每个消息产生的相关数据。为了避免单个会话在同一个时间戳产生重复数据,可以把时间戳粒度细化到微秒。其他消息相关数据,作为属性写入。对于每个会话的不同消息,建议根据时间戳分开存储。虽然 DynamoDB 支持 JSON 格式,但是单个 Item 最大支持 400KB,多条消息放在一个 Item 可能会超过此限制,并且以 JSON 格式存储,不能利用索引加速查询,不建议在消息量大的时候使用。这里还使用了 status_ts 的属性,类似 active#1715324385081,标记会话状态和时间戳,以供创建相关索引使用。

数据示例如下:

对于业务表的查询,可以按照 chatid+ts 的点查方式。

  • 获取某个会话的所有数据:Query (chatid)
  • 获取某个会话特定时间段数据:Query (chatid + ts between t1 and t2)

除了业务基表主键之外的加速查询,需要根据需要创建二级索引,一般使用全局二级索引(Global Secondary Index, GSI),不受分区限制,并且更灵活。本质上全局二级索引是从源表异步复制的另外一张表,也有自己的读写容量。

GSI 索引查询设计参考:

分别以发送者和接收者为分区键创建索引,并加上时间戳作为排序键。

  • 查询发送者或者接收者的所有聊天消息

Query (sender_id/receiver_id)

  • 查询某个时间范围发送者或者接收者的聊天消息:

Query (sender_id/receiver_id + ts between t1 and t2)

  • 创建 GSI,获取某个聊天下所有活动的会话

Query (chatid + status_ts begin with “active”)

除了以上实时聊天消息读写之外,社交应用还需要维护用户关注列表和粉丝。普通的数据库可以考虑以下设计:

字段名 类型 内容
id int 关系 id
user_id int 用户
following List 关注列表
follower List 粉丝列表
created_at timestamp 创建时间
updated_at timestamp 更新时间

此设计中,每个用户,有自己的关注和粉丝列表,方便业务逻辑处理。但是,当关注和粉丝过多时,影响查询效率。Redis Hash 类型适合处理此类数据,内存性能更快,适合大 V 这种粉丝很多的用户。但是,Redis 毕竟是缓存,存在数据丢失风险,而 Amazon DynamoDB 解决了数据持久性问题。

在社交网络实体关系表中,用户作为中心实体,分别有各自的关注和粉丝,以及时间线、帖子、喜欢、收藏等功能。

依照此关系图,可以进行相关的表设计。具体请参考 Social network schema design in DynamoDB

总结

  • 社交实时聊天消息的数据库,需要兼顾高性能与持久化,可以使用 Amazon DynamoDB,在保证数据持久化的同时,海量请求下仍然能达到毫秒级别的响应速度。
  • 根据业务需求设计 Amazon DynamoDB 表,会话可以作为中心,加上时间排序,组合主键适合实时聊天消息存储。
  • 结合全局二级索引,实现包括用户、时间、状态等更多维度的业务查询。
  • 社交网络关系列表,也可以使用 Amazon DynamoDB 数据库。

参考链接

https://zhuanlan.zhihu.com/p/409927117

https://www.jianshu.com/p/d4fff41c7c3e

本篇作者

章平

亚马逊云科技数据库架构师。2014 年起就职于亚马逊云科技,先后加入技术支持和解决方案团队,致力于客户业务在云上高效落地。对于各类云计算产品和技术,特别是在数据库和大数据方面,拥有丰富的技术实践和行业解决方案经验。此前曾就职于 Sun,Oracle,Intel 等 IT 企业。