类似微信朋友圈业务的数据库表结构设计

yumo6662个月前 (03-20)技术文章14

今天聊一下类似微信朋友圈这样的业务该怎么设计数据库的表结构,纯个人观点,希望能够提供一点参考价值。

数据库使用 MongoDB,因为 MongoDB 比较灵活,支持属性层级较复杂的数据存储,并且可以为这些属性设置索引,有些情况使用 mysql 可能要创建不少关联表然后联表查询,使用 MongoDB 程序能更简洁一些。此外 MongoDB 支持分片集群,对于数据量大的业务承载能力更好一些,下面涉及创建集合(相当于mysql中的表)的部分也会有分区键。

约定

为了直观易读,所有的集合中的数据结构都使用 TypeScript 的接口定义来表示。由于习惯问题,后面出现的 mongo 的集合,统一称作为表。

联系人

联系人表记录自己有哪些好友。

interface contact {
    _id: string
    /**
     * 用户id.
     */
    userId: string
    /**
     * 好友id.
     */
    contactId: string
    // 其它属省略,不会出现在后面的查询中
}

创建唯一索引:

db.contact.createIndex({userId: 1, contactId: 1}, {unique: true})

联系人标签

微信中有个给联系人打标签的功能,朋友圈动态可以使用标签来设置权限,所以我们要创建一个联系人标签表。

interface contactTag {
  _id: string;
  /**
   * 创建人ID.
   */
  creatorId:string;
  /**
   * 用户ID.
   */
  userId: string;
  /**
   * 标签ID.
   */
  tagId: string;
}

创建唯一索引,将 userId 放在前面,方便使用 userId 查询自己被好友打上了哪些标签。

db.contactTag.createIndex({userId: 1, tagId: 1}, {unique: true})

联系人权限

微信中可以对联系人设置朋友圈权限:不看他(她)、不让他(她)看我。所以我们增加一个权限设置表。

interface momentSetting {
  /**
   * 与用户id一对一关联.
   */
  _id: string
  /**
   * 被排除的用户ID,不看他(她).
   */
  excludedUserIds: string[]
  /**
   * 反向排除的用户ID,不让他(她)看我.
   */
  reverseExcludedUserIds: string[]
}

为 reverseExcludedUserIds 创建索引,用于查询自己被哪些好友屏蔽。

db.momentSetting.createIndex({reverseExcludedUserIds: 1})

朋友圈的动态可以设置根据群设置权限,所以必须得得增加群用户这个表。

interface groupUser {
  _id: string
  /**
   * 微信群id.
   */
  groupId: string
  /**
   * 用户id.
   */
  userId: string
}

创建唯一索引,将 userId 放在前面,方便查询自己所在的群。

db.groupUser.createIndex({userId: 1, groupId: 1}, {unique: true})

朋友圈动态

微信朋友圈在发动态时,可以设置谁可以看:公共(所有人)、私密(仅自己)、部分可见(可设置人、群和标签)、不给谁看(可设置人、群和标签)。这些设置是单选的,只能选择一个。

下面是动态表的数据结构:

interface moment {
    /**
     * 带有时间顺序的id,由时间戮加随机数生成.
     */
    _id: string
    /**
     * 作者id.
     */
    creatorId: string
    /**
     * 开放类型,公开、私密,部分人等. public 表示公开,private 表示私密,
     * partly-visible 表示部分可见,partly-invisible 表示部分人不可见(不让谁看).
     * 当值为 partly-visible 或 partly-invisible,下面的几个属性才有效.
     */
    openType: 'public' | 'private' | 'partly-visible' | 'partly-invisible',
    /**
     * 受限或开放的群组id列表.
     */
    groupIds: string[]
    /**
     * 受限或开放的标签id列表.
     */
    tagIds: string[]
    /**
     * 受限或开放的用户id列表.
     */
    userIds: string[]
    // 其它的属性,如内容、创建时间等字段此处省略,这些字段不出现在查询语句中
}

本文主要说明如何做查询,内容等字段不出现在查询语句中,此处省略,不做相关的介绍。然后我们为创建人id和_id创建索引,以方便后面按时间顺序查询记录。

db.moment.createIndex({creatorId: 1, _id: -1})

动态表可以将 creatorId 设置为分区键,这样不同的用户的数据会分布在不同的节点上,在写入数据时能避免热片问题。

sh.shardCollection( "库名.moment", { "creatorId" : "hashed" } )

查询流程

相关的表都已经创建好了,现在可以开始做查询逻辑了。

获取朋友圈可见的好友ID列表

从联系人集合中查询当前用户所有好友的ID,在程序中存为 contactIds

db.contact.find({userId:'当前用户ID'},{contactId:1})

从动态设置集合中查询当前用户被哪些好友屏蔽,得到这些好友的ID,在程序中存为 unfriendlyContactIds 。

db.momentSetting.find({reverseExcludedUserIds:'当前用户ID'},{_id:1})

从动态设置表中查询当前用户屏蔽了哪些好友,得到这些好友的ID,在程序中存为 excludedUserIds 。

db.momentSetting.find({_id:'当前用户ID'},{excludedUserIds:1})

然后从 contactIds 删除掉 unfriendlyContactIds 和 excludedUserIds 中包含的用户ID,得到所有可以查看朋友圈的好友ID,在程序中记为 friendlyContactIds

获取所有标签ID

从联系人标签集合中查询当前用户被好友打上的所有标签的ID,在程序中存为 tagIds 。

db.contactTag.find({userId:'当前用户ID'},{tagId:1})

获取所有群组ID

从君用户集合中查询当前用户所在的所有群ID,在程序中记为 groupIds

db.groupUser.find({userId:'当前用户ID'},{groupId:1})

查询动态表

经过前面的几次查询,我们得到以下数据:

变量名称

说明

friendlyContactIds

朋友圈可见的好友id列表

tagIds

被所有好友打上的标签id列表

groupIds

加入的所有群组id列表

有了这些信息,就可以从动态集合(表)中查询记录了。上面的信息不需要每次都重新查询一次,可以查询后缓存一段时间,一般情况下很少会有改动,加载新动态时可直接复用。

db.moment.find({
    creatorId: {$in: '插入 friendlyContactIds'}
    _id: {$gt: '上次查询最后一条记录的ID'},
    $or: [
        {openType: 'public'},
        {
            openType: 'partly-visible',
            $or: [
                {groupIds: {$in: '插入 groupIds'}},
                {tagIds: {$in: '插入 tagIds'}},
                {userIds: '当前用户ID'}
            ]
        },
        {
            openType: 'partly-invisible',
            $and: [
                {groupIds: {$nin: '插入 groupIds'}},
                {tagIds: {$nin: '插入 tagIds'}},
                {userIds: {$ne: '当前用户ID'}}
            ]
        }
    ]
}).sort({_id: -1}).limit(20)

上面的查询语句中需要程序中动态插入前面查询到的数据记录,_id 的过虑条件用于翻页,首次查询不需要。查询主要依赖于动态表的字段 creatorId 和 _id 联合索引,群组ID、标签ID和用户ID没有必要创建索引,意义不大。

总结

整个流程没有什么复杂的查询,仅最后查询动态表稍微麻烦一点点。随着好友数量的增加,查询的效率会有所降低,所以业务上需要限制好友的数量。

相关文章

值得一看的PG数据库query流程

概述今天主要分享一个PG数据库的QUERY处理流程,建议掌握,后面做优化还是有用的。流程图:流程解析第一步:客户端程序可以是任何符合 PostgreSQL 协议规范的程序,如 JDBC 驱动。Post...

历史数据库的设计与实现

摘 要: 针对Wonderware平台自带的历史数据服务无法满足大量历史数据同时存储的问题,对该平台的历史数据服务进行了研究和分析,在保证Wonderware平台存储历史数据的正确性、及时性和完整性的...

数据库工程师证考试流程?

一、确定证书类型和考试机构首先,考生需要明确自己想要获得的证书类型,如初级、中级或高级。不同的等级对应不同的考试难度和内容。接着,考生需要查找并确定相应的考试机构。这些考试机构通常会提供详细的考试要求...

积攒十五年的MySQL数据库设计经验,拿走不谢

前言关系型数据库是当前广泛应用的数据库类型,关系数据库设计是对数据进行组织化和结构化的过程,核心问题是关系模型的设计。对于数据库规模较小的情况,我们可以比较轻松地处理数据库中的表结构。然而,随着项目规...

鸿博档案:档案数字化基本流程是什么

一、术语和定义1、数字化:用计算机技术将模拟信号转换为数字信号的处理过程。2、纸质档案数字化:采用扫描仪或数码相机等数码设备对纸质档案进行数字加工,将其转化为存储在磁带、磁盘、光盘等载体上并能被计算机...

DeepSeek+dify知识库,查询数据库的两种方式(api+直连)

自从发了 DeepSeek+dify 本地知识库:真的太香了这篇以后,一直有小伙伴介绍在问我,怎么让在个ai应用客户端直接连接数据库查询。dify官方没有现成的组件可以直接用。当时我想的是两种方式,一...