了解带有表情符号和回复的 Slack 聊天数据库设计架构

waq*_*waq 3 sql database postgresql mongodb cassandra

我正在尝试构建一个类似于 slack 聊天的聊天应用程序,我想了解他们是如何设计数据库的,当有人加载聊天时,它会立即返回如此多的信息,哪个数据库适合这个问题,我添加了屏幕截图一样的,供参考。

闲聊

最初,当我开始考虑这个问题时,我想继续使用 PostgreSQL 并始终保持表规范化以保持干净,但随着我继续进行,规范化开始感觉像是一个问题。

用户表

ID 姓名 电子邮件
1 约翰 约翰@gmail.com
2 相同的 sam@gmail.com

频道表

ID 频道名称
1 频道名称1
2 频道名称2

参加者表

ID 用户身份 频道号
1 1 1
2 1 2
3 2 1

聊天桌

ID 用户身份 频道号 父 ID 消息文本 回复总数 时间戳
1 1 1 无效的 第一条消息 0 -
2 1 2 1 第二条消息 10 -
3 1 3 无效的 第三条消息 0 -

聊天表的列名称为parent_id,它告诉我它是父消息还是子消息我不想使用递归子消息,所以这很好

表情符号表

ID 用户身份 消息ID emoji_uni 代码
1 1 12 U123
2 1 12 U234
3 2 14 U456
4 2 14 U7878
5 3 14 U678

一个人可以对同一条消息使用多个表情符号做出反应

当有人加载时,我想获取插入表中的最后 10 条消息,其中包含对每条消息和回复做出反应的所有表情符号,就像您在图像中看到的那样,其中显示 1 个带有个人资料图片的回复(这可以超过 1 个)

现在要获取这些数据,我必须连接所有表,然后获取数据,考虑到这种情况会非常频繁,这在后端可能是一项非常繁重的工作。

我的想法是我会在聊天表中添加两列,即 profile_replies 和 emoji_reactions_count ,两者都是bson数据类型来存储类似这样的数据

这适用于 emoji_reactions_count 列

这也有两种方式,一种是仅计数方式

{
  "U123": "123",// count of reactions on an emoji
  "U234": "12"
}
Run Code Online (Sandbox Code Playgroud)

当有人做出反应时,我会更新计数并从表情符号表中插入或删除行,这里我有一个问题,任何消息上的表情符号更新过于频繁可能会变得很慢?因为每次有人用表情符号做出反应时我都需要更新上表中的计数

或者

像这样存储用户 ID 和计数,这看起来更好我可以完全摆脱表情符号表

{
  "U123": {
    "count": 123, // count of reactions on an emoji
    "userIds": [1,2,3,4], // list of users ids who all have reacted
  },
  "U234": {
    "count": 12,
    "userIds": [1,2,3,4],
  },
}
Run Code Online (Sandbox Code Playgroud)

这适用于 profile_replies 列

[
  {
    "name": 'john',
    "profile_image": 'image url',
    "replied_on": timestamp
  },
  ... with similar other objects
]
Run Code Online (Sandbox Code Playgroud)

这看起来是不错的解决方案吗?或者我可以做些什么来改进,或者我应该切换到一些 noSQL 数据库,如 mongodb 或 cassandra?我考虑过 mongodb,但这看起来也不是很好,因为当数据呈指数级增长时,连接速度很慢,但相对而言,这种情况在 sql 中不会发生。

小智 6

尽管这实际上更像是一次讨论,而且这样的问题没有完美的答案,但我会尝试指出重建 Slack 时您可能需要考虑的事项:

  1. 表情符号表:正如@Alex Blex 所言,对于一款聊天软件来说,已经可以忽略不计了。稍后,它们可以由应用程序中的某些缓存注入,或者在中间件或视图中的某个位置或任何地方,或者直接与消息一起存储。无需在数据库端加入任何内容。
  2. 工作区:Slack 在工作区中组织,您可以在其中与同一用户一起参与。每个工作区可以有多个频道,每个频道可以有多个客人。每个用户都可以加入多个工作区(作为管理员、正式成员、单通道或多通道访客)。尝试从这个想法开始。
  3. 频道:我会将频道措辞重构为例如对话,因为基本上(这里是个人意见)我认为例如具有 10 名成员的频道和涉及 5 个人的定向对话之间没有太大区别,除了以下事实:用户可以稍后加入(打开)频道并查看之前的消息,这对于关闭的频道和直接消息来说是不可能的。

现在针对您的实际数据库布局问题:

  • 当您稍后开发包含各种统计信息的管理仪表板时,添加像reply_count或profile_replies这样的列会非常方便,但客户端绝对不需要。
  • 假设您的客户端在加入/启动客户端时进行了一次小调用“获取工作区成员”(然后显然频繁地更新客户端上的缓存),则无需将用户数据与消息一起存储,即使有 1000 个成员在同一个工作空间上,它应该只有几个 MiB 的信息。
  • 假设您的客户通过调用“获取最近的工作空间对话”(当然您可以通过是否公开和加入进行过滤)执行相同的操作,您将获得一个很好的列表,其中包含您已经加入的频道以及您最近交谈过的人到。
create table message
(
    id bigserial primary key,
    workspace_id      bigint                   not null,
    conversation_id   bigint                   not null,
    parent_id         bigint,
    created_dt        timestamp with time zone not null,
    modified_at       timestamp with time zone,
    is_deleted        bool not null default false,
    content           jsonb
)
    partition by hash (workspace_id);


create table message_p0 partition of message for values with (modulus 32, remainder 0);
create table message_p1 partition of message for values with (modulus 32, remainder 1);
create table message_p2 partition of message for values with (modulus 32, remainder 2);
...
Run Code Online (Sandbox Code Playgroud)

因此,基本上,每当用户加入新对话时,您对数据库的查询将是:

SELECT * FROM message WHERE workspace_id = 1234 ORDER BY created_dt DESC LIMIT 25;
Run Code Online (Sandbox Code Playgroud)

当你开始向上滚动时,它会是:

SELECT * FROM message WHERE workspace_id = 1234 AND conversation_id = 1234 and id < 123456789 ORDER BY created_dt DESC LIMIT 25;
Run Code Online (Sandbox Code Playgroud)

等等...正如您已经看到的,如果您另外添加一个索引(如果您使用分区,则可能会有所不同),您现在可以通过工作区和对话非常有效地选择消息:

create index idx_message_by_workspace_conversation_date
    on message (workspace_id, conversation_id, created_dt)
    where (is_deleted = false);
Run Code Online (Sandbox Code Playgroud)

对于消息格式,我会使用与 Twitter 类似的格式,有关更多详细信息,请查看他们的官方文档: https ://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/tweet

当然,例如您的客户端 v14 应该知道如何“渲染”从 v1 到 v14 的所有对象,但这就是消息格式版本控制的伟大之处:它是向后兼容的,您可以随时启动支持更多功能的新格式,的原始示例content可能是:

{
  "format": "1.0",
  "message":"Hello World",
  "can_reply":true,
  "can_share":false,
  "image": {
     "highres": { "url": "https://www.google.com", "width": 1280, "height": 720 },
     "thumbnail": { "url": "https://www.google.com", "width": 320, "height": 240 }
  },
  "video": null,
  "from_user": {
    "id": 12325512,
    "avatar": "https://www.google.com"
  }
}
Run Code Online (Sandbox Code Playgroud)

在我看来,一个非常复杂的问题是有效地确定每个用户已阅读哪些消息。我不会详细介绍如何发送推送通知,因为这应该由后端应用程序完成,而不是通过轮询数据库来完成。使用之前从“获取最近的工作区对话”收集的数据(类似于SELECT * FROM user_conversations ORDER BY last_read_dt DESC LIMIT 25应该做的事情,在您的情况下,您必须在参与者表中添加last_read_message_id和last_read_dt),然后您可以执行查询以获取哪些消息尚未被读取:

  • 返回消息的小存储函数
  • 返回这些消息的 JOIN 语句
  • 返回这些消息的 UNNEST / LATERAL 语句
  • 也许我现在还没有想到其他的事情。:)

最后但并非最不重要的一点是,我强烈建议不要尝试重建 Slack,因为还有很多主题需要涵盖,例如安全性和加密、API 和集成等等......