在上一篇文章中,我对社交朋友圈的设计进行了大致的分析,并确定了分库分表、索引优化等方案。接下来我将基于这些设计方案,通过代码示例来实现朋友圈内容的发布 和查询流程,从而展示整个方案的实际操作过程。
我将用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存在以下几个问题:
- 自增ID的分布不均:由于多个表中分别自增,可能会导致ID不唯一,且不同库或表的ID顺序可能不连续,增加了合并查询的复杂性。
- 分布式环境中的冲突:自增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_at
和like_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张表 ,用于支撑系统的长期数据存储需求。然而,考虑到数据库和表的数量,直接提前创建所有库和表并不实际,且会浪费资源。因此,为了更高效地管理分库分表,采用动态创建的方式更加合理。
为什么选择动态创建?
- 减少资源浪费:不提前创建所有库和表,而是根据实际的数据量动态扩展,这样能在保证存储需求的前提下,减少不必要的资源消耗。
- 提升灵活性:动态创建库和表允许系统在实际需求增长时逐步扩展,避免一次性创建大量空表带来的管理和维护压力。
- 更好的分库分表管理:动态创建库和表可以结合数据量的增长策略(如定期检查数据量是否达到阈值),当达到预设条件时再创建新的库和表,以确保分布均匀并优化数据存储性能。
通过这种方式,分库分表的逻辑更具弹性和友好性,不仅节省了资源,还可以确保系统扩展性和维护的简便性。
为了实现分库分表的弹性扩展,我们可以编写一个动态创建库和表的逻辑。该逻辑会在需要时自动创建新的数据库和表,以满足不断增长的数据存储需求。完整代码如下:
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
封装了动态创建数据库和数据表的逻辑,包括配置数据库连接、动态创建库表、以及检查表是否存在等一系列方法。以下是主要的功能:
-
获取数据库配置 :
getDatabaseConfig
方法从.env
文件读取数据库配置,如主机、端口、用户名等信息,并根据传入的数据库名称生成对应配置。 -
动态创建数据库连接 :
createConnection
方法通过ConnectionFactory
动态生成数据库连接,以确保数据库按需动态创建。 -
创建数据库和数据表:
createDatabaseIfNotExists
方法检查并创建数据库,如果数据库不存在则创建它。createTableIfNotExists
方法检查表是否存在,若不存在则根据指定结构创建表。
-
检查表是否存在 :
tableExists
方法用于检查表的存在性,以避免重复创建。 -
分库分表逻辑 :
getDatabaseAndTable
方法基于user_id
计算数据库和表的索引,以动态分配数据到不同的库表中。使用了user_id % 分库数
来确定数据库,(user_id + db_index) % 分表数
来确定表,保证了数据的均匀分布。
这样设计的DatabaseTableTrait
,能够便捷地进行分库分表的管理和数据库连接的创建,方便在需要的地方引入该trait,进行高效的数据库操作。
至此,关于该朋友圈分库分表设计方案的数据库设计部分已基本完成。我们定义了动态分库分表的逻辑,实现了数据库和数据表的动态创建,以及全局排序索引表的设计,确保数据的分散存储和高效查询。
接下来,我们将考虑缓存设计在该架构中的必要性,并探讨适合该场景的缓存策略,以进一步优化系统的查询效率和响应速度。