论如何设计一个社交朋友圈内容架构(二)

在上一篇文章中,我对社交朋友圈的设计进行了大致的分析,并确定了分库分表、索引优化等方案。接下来我将基于这些设计方案,通过代码示例来实现朋友圈内容的发布查询流程,从而展示整个方案的实际操作过程。

我将用PHP语言的Hyperf协程框架具体设置数据库连接和分库分表规则,接着会编写发布和查询的核心代码。通过这些步骤,可以比较直观地了解如何在高并发和大数据量的场景下,实现一个高效的社交朋友圈子系统。

分库分表规则的制定

首先,我们需要制定分库分表的逻辑。在这个设计方案中,通过**user_id**来决定数据的存储位置。具体来说,**通过user_id % 分库数来确定应存入的数据库,接着再使用 user_id % 分表数**来决定进入哪个表。

以下是基于PHP语言实现这部分逻辑的示例代码:

PHP 复制代码
/**
 * 获取数据库和表名
 *
 * @param int $user_id
 * @return array
 */
public function getDatabaseAndTable($user_id): array
{
    $db_count = 10;  // 假设有10个库
    $table_count = 100;  // 每个库有100张表

    // 计算数据库索引
    $db_index = $user_id % $db_count;
    $db_name = 'circle_db_' . $db_index; // 添加 db_ 区分不同数据库

    // 计算表索引
    $table_index = ($user_id + $db_index) % $table_count; // 使用 db_index 做一点变动,避免 db 和 table 完全一致
    $table_name = 'circle_table_' . $table_index; // 添加 table_ 区分不同表

    return ['db' => $db_name, 'table' => $table_name];
}

表结构设计

在设计朋友圈功能时,我们首先需要确定存储每条发布内容的表结构。对于每条朋友圈数据,以下字段是必不可少的:

  • user_id:标识发布内容的用户ID,方便根据用户进行查询和管理。
  • content:用户发布的文字内容,存储帖子中的文本信息。
  • media_url:用户发布的媒体内容URL,用于指向图片或视频资源。
  • visibility:内容的可见性,表示内容对谁可见(0:公开,1:好友可见,2:仅自己可见)。
  • created_at:记录内容的发布时间,方便按时间排序和查询。

在分库分表的设计中,主键的选择也是一个关键点。传统的递增ID在单库单表的情况下使用广泛,因其自增特性便于查询排序。然而,在分库分表场景下,自增ID存在以下几个问题:

  1. 自增ID的分布不均:由于多个表中分别自增,可能会导致ID不唯一,且不同库或表的ID顺序可能不连续,增加了合并查询的复杂性。
  2. 分布式环境中的冲突:自增ID在分布式环境下可能会产生冲突,无法确保全局唯一性。

因此,分布式ID 成为了更好的选择。分布式ID通常使用UUID或雪花算法(Snowflake)生成,这些算法能够保证在多库多表环境下的唯一性和有序性。其中,雪花算法生成的ID包含了时间戳信息,具有一定的递增性,便于按时间排序。

综合考虑后,建议在分库分表的朋友圈表中采用分布式ID作为主键,以确保各库表中主键的唯一性,且避免因分表导致的ID冲突问题。

基于上述分析,我们确定了朋友圈内容的表结构,具体结构如下:

sql 复制代码
CREATE TABLE `circle_table_xx` (
  `snowflake_id` bigint(20) unsigned NOT NULL COMMENT '雪花ID,主键',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID,标识发布内容的用户',
  `content` text NOT NULL COMMENT '用户发布的文字内容',
  `media_url` varchar(512) DEFAULT NULL COMMENT '用户发布的媒体内容(图片或视频)的URL',
  `visibility` tinyint(4) NOT NULL DEFAULT '0' COMMENT '内容的可见性(0:公开, 1:好友可见, 2:仅自己可见)',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间,内容发布时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录最后更新时间',
  PRIMARY KEY (`snowflake_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户发布内容表(动态/朋友圈)';

全局排序索引表的设计

在分库分表的环境下,如果需要按时间排序查询推荐列表,会遇到数据分散导致的全局排序困难。通常情况下,无法直接在多个分库分表中高效地进行跨表时间排序。因此,为了实现推荐列表的查询,我们需要引入一个全局排序机制 ,通过全局排序索引表来存储和维护帖子排序。 这张全局索引表的设计思路是:

  • 专门存储帖子ID、用户ID、点赞量以及创建时间,用于支持全局排序。
  • 推荐列表的查询将直接从此索引表中获取最新的帖子,按时间或点赞量排序,从而避免在分库分表中进行高成本的跨表查询。
  • 获取推荐列表时,仅从索引表中获取基础信息,再通过circle_id定位到具体的分库分表,查询帖子详细内容。

这种结构不仅提高了查询效率,还能灵活地满足不同排序需求(如最新发布或点赞量最高的帖子)。

全局排序索引表结构设计

为支持推荐列表的高效查询,我们设计了全局排序索引表,存储基本的排序信息,字段如下:

  • circle_id:圈子ID,用于唯一标识每条帖子,与具体分表中的帖子记录一一对应。
  • user_id:发布该帖子的用户ID。
  • like_count:点赞量,用于按点赞数进行排序查询。
  • created_at:帖子创建时间,用于按发布时间排序。

索引设计

由于推荐列表查询需求主要是按时间点赞量排序,因此需要在以下字段上加索引:

  • created_at索引:支持按时间排序查询,以便获取最新的帖子。
  • like_count索引:支持按点赞数进行排序查询,便于获取点赞量较高的帖子。

通过给created_atlike_count加索引,能够显著提升查询效率,使系统在生成推荐列表时可以快速从索引表中提取所需数据。

以下是该表的表结构代码:

sql 复制代码
CREATE TABLE `circle_global` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `circle_id` bigint(20) NOT NULL,
  `user_id` bigint(20) NOT NULL,
  `created_at` datetime NOT NULL,
  `like_count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_created_at` (`created_at`),
  KEY `idx_like_count` (`like_count`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

动态创建数据库和数据表的需求

根据之前的分析需求,我们的目标是10个库 ,每个库包含100张表 ,用于支撑系统的长期数据存储需求。然而,考虑到数据库和表的数量,直接提前创建所有库和表并不实际,且会浪费资源。因此,为了更高效地管理分库分表,采用动态创建的方式更加合理。

为什么选择动态创建?

  1. 减少资源浪费:不提前创建所有库和表,而是根据实际的数据量动态扩展,这样能在保证存储需求的前提下,减少不必要的资源消耗。
  2. 提升灵活性:动态创建库和表允许系统在实际需求增长时逐步扩展,避免一次性创建大量空表带来的管理和维护压力。
  3. 更好的分库分表管理:动态创建库和表可以结合数据量的增长策略(如定期检查数据量是否达到阈值),当达到预设条件时再创建新的库和表,以确保分布均匀并优化数据存储性能。

通过这种方式,分库分表的逻辑更具弹性和友好性,不仅节省了资源,还可以确保系统扩展性和维护的简便性。

为了实现分库分表的弹性扩展,我们可以编写一个动态创建库和表的逻辑。该逻辑会在需要时自动创建新的数据库和表,以满足不断增长的数据存储需求。完整代码如下:

PHP 复制代码
<?php
declare(strict_types=1);

namespace App\Traits;

use App\Db\CustomConnector;
use Hyperf\Database\Connection;
use Hyperf\Database\Connectors\ConnectionFactory;
use Hyperf\DbConnection\Db;

/**
 * Trait DatabaseTableTrait
 * @package App\Traits
 */
trait DatabaseTableTrait
{

    /**
     * 获取默认的数据库配置,除了数据库名称其他都相同,配置从 env 文件读取
     *
     * @param string $db_name 数据库名称
     * @return array 数据库配置
     */
    protected function getDatabaseConfig(string $db_name): array
    {
        return [
            'driver' => env('DB_DRIVER', 'mysql'), // 从 .env 文件读取数据库驱动,默认为 mysql
            'host' => env('DB_HOST', '127.0.0.1'), // 从 .env 文件读取 host,默认为 127.0.0.1
            'port' => env('DB_PORT', 3306), // 读取端口号,默认为 3306
            'database' => $db_name, // 传入的数据库名称
            'username' => env('DB_USERNAME', 'root'), // 从 .env 文件读取用户名
            'password' => env('DB_PASSWORD', ''), // 从 .env 文件读取密码
            'charset' => env('DB_CHARSET', 'utf8mb4'), // 读取字符集,默认为 utf8mb4
            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), // 读取字符集排序规则
            'prefix' => env('DB_PREFIX', ''), // 表前缀,默认为空
        ];
    }

    /**
     * 动态创建连接
     *
     * @param array $config 数据库配置
     * @param ConnectionFactory $factory 连接工厂
     * @return Connection
     */
    public function createConnection(array $config, ConnectionFactory $factory): Connection
    {
        // 创建自定义的数据库连接器
        $connector = new CustomConnector($factory);
        return $connector->connect($config);
    }


    /**
     * 创建数据库表
     *
     * @param string $table_name
     * @param \Closure $callback
     * @return void
     */
    public function createDatabaseIfNotExists($db_name): void
    {
        try {
            // 切换到默认数据库连接,执行数据库存在性检查和创建
            $query = "CREATE DATABASE IF NOT EXISTS `{$db_name}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
            Db::connection('default')->statement($query);
        } catch (\Exception $e) {
            // 捕获并处理异常
            throw new \Exception("数据库创建失败: " . $e->getMessage());
        }
    }

    /**
     * 动态创建数据库连接并检查表是否存在,若不存在则创建
     *
     * @param string $db_name 数据库名称
     * @param string $table_name 表名称
     * @param ConnectionFactory $factory 连接工厂
     * @return void
     * @throws \Exception
     */
    public function createTableIfNotExists(string $db_name, string $table_name, ConnectionFactory $factory): void
    {
        try {
            // 获取数据库配置
            $config = $this->getDatabaseConfig($db_name);

            // 动态创建数据库连接
            $connection = $this->createConnection($config, $factory);

            // 检查表是否已存在
            if ($this->tableExists($db_name, $table_name, $connection)) {
                return; // 如果表已存在,直接返回,避免重复创建
            }

            // 构建创建表的 SQL 语句
            $create_table_query = sprintf("
                CREATE TABLE IF NOT EXISTS `%s` (
    `snowflake_id` BIGINT UNSIGNED PRIMARY KEY COMMENT '雪花ID,主键',
    `user_id` BIGINT NOT NULL COMMENT '用户ID,标识发布内容的用户',
    `content` TEXT NOT NULL COMMENT '用户发布的文字内容',
    `media_url` VARCHAR(512) COMMENT '用户发布的媒体内容(图片或视频)的URL',
    `visibility` TINYINT NOT NULL DEFAULT 0 COMMENT '内容的可见性(0:公开, 1:好友可见, 2:仅自己可见)',
    `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间,内容发布时间',
    `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录最后更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户发布内容表(动态/朋友圈)';
            ", $table_name);

            // 执行创建表的 SQL 语句
            $connection->statement($create_table_query);
        } catch (\Exception $e) {
            // 捕获并抛出异常
            throw new \Exception("表创建失败: " . $e->getMessage());
        }
    }

    /**
     * 检查表是否存在
     *
     * @param string $db_name
     * @param string $table_name
     * @param Connection $connection
     * @return bool
     */
    public function tableExists(string $db_name, string $table_name, $connection): bool
    {
        try {
            // 拼接查询 SQL 语句
            $query = sprintf("SHOW TABLES LIKE '%s'", $table_name);

            // 执行查询,检查表是否存在
            $result = $connection->select($query);

            return count($result) > 0;
        } catch (\Exception $e) {
            throw new \Exception("检查表是否存在时失败: " . $e->getMessage());
        }
    }



    /**
     * 获取数据库和表名
     *
     * @param int $user_id
     * @return array
     */
    public function getDatabaseAndTable($user_id): array
    {
        $db_count = 10;  // 假设有10个库
        $table_count = 100;  // 每个库有100张表

        // 计算数据库索引
        $db_index = $user_id % $db_count;
        $db_name = 'circle_db_' . $db_index; // 添加 db_ 区分不同数据库

        // 计算表索引
        $table_index = ($user_id + $db_index) % $table_count; // 使用 db_index 做一点变动,避免 db 和 table 完全一致
        $table_name = 'circle_table_' . $table_index; // 添加 table_ 区分不同表

        return ['db' => $db_name, 'table' => $table_name];
    }


}

为了更好地管理和使用数据库相关操作,我将分库分表的操作封装成了一个trait ,这样在项目中可以方便地引入和操作。这个trait封装了动态创建数据库和数据表的逻辑,包括配置数据库连接、动态创建库表、以及检查表是否存在等一系列方法。以下是主要的功能:

  1. 获取数据库配置getDatabaseConfig方法从.env文件读取数据库配置,如主机、端口、用户名等信息,并根据传入的数据库名称生成对应配置。

  2. 动态创建数据库连接createConnection方法通过ConnectionFactory动态生成数据库连接,以确保数据库按需动态创建。

  3. 创建数据库和数据表

    • createDatabaseIfNotExists方法检查并创建数据库,如果数据库不存在则创建它。
    • createTableIfNotExists方法检查表是否存在,若不存在则根据指定结构创建表。
  4. 检查表是否存在tableExists方法用于检查表的存在性,以避免重复创建。

  5. 分库分表逻辑getDatabaseAndTable方法基于user_id计算数据库和表的索引,以动态分配数据到不同的库表中。使用了user_id % 分库数来确定数据库,(user_id + db_index) % 分表数来确定表,保证了数据的均匀分布。

这样设计的DatabaseTableTrait,能够便捷地进行分库分表的管理和数据库连接的创建,方便在需要的地方引入该trait,进行高效的数据库操作。

至此,关于该朋友圈分库分表设计方案的数据库设计部分已基本完成。我们定义了动态分库分表的逻辑,实现了数据库和数据表的动态创建,以及全局排序索引表的设计,确保数据的分散存储和高效查询。

接下来,我们将考虑缓存设计在该架构中的必要性,并探讨适合该场景的缓存策略,以进一步优化系统的查询效率和响应速度。

相关推荐
全栈派森1 小时前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse1 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭3 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架3 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱3 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜3 小时前
Flask框架搭建
后端·python·flask
进击的雷神3 小时前
Perl语言深度考查:从文本处理到正则表达式的全面掌握
开发语言·后端·scala
进击的雷神3 小时前
Perl测试起步:从零到精通的完整指南
开发语言·后端·scala
豌豆花下猫4 小时前
Python 潮流周刊#102:微软裁员 Faster CPython 团队(摘要)
后端·python·ai
秋野酱4 小时前
基于javaweb的SpringBoot驾校预约学习系统设计与实现(源码+文档+部署讲解)
spring boot·后端·学习