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

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

我将用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,进行高效的数据库操作。

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

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

相关推荐
疯一样的码农17 分钟前
Spring Boot Starter Parent介绍
java·spring boot·后端
爱上语文22 分钟前
Springboot 阿里云对象存储OSS 工具类
java·开发语言·spring boot·后端·阿里云
小黑0325 分钟前
Scala第三天
开发语言·后端·scala
茶馆大橘3 小时前
Spring Validation —— 参数校验框架
java·后端·学习·spring
qq_172805596 小时前
Go 性能剖析工具 pprof 与 Graphviz 教程
开发语言·后端·golang·go
丶21366 小时前
【SQL】掌握SQL查询技巧:数据分组与排序
数据库·后端·sql
哈哈哈哈cwl8 小时前
还搞不明白浏览器缓存?
后端·node.js·浏览器
橘子海全栈攻城狮9 小时前
【源码+文档+调试讲解】基于Android的固定资产借用管理平台
android·java·spring boot·后端·python·美食
Stark、10 小时前
《数据结构》--队列【各种实现,算法推荐】
开发语言·数据结构·c++·后端·算法