Laravel 10 水平分表优化
Laravel10水平分表优化核心: 解耦、自动化、高性能、可监控
- 封装
BaseShardingModel基类,消除重复代码,统一分表逻辑,业务模型极简接入; - 实现通用定时任务 + 建表命令,自动化创建分表,避免手动操作失误;
- 遵循分表查询原则,使用
unionAll优化跨分表查询,批量操作提升写入效率; - 兼容 Laravel ORM 高级特性(软删除、查询作用域),不改变原有开发习惯;
- 增加分表监控与批量备份,保障生产环境的稳定性和数据安全性。
核心优化 1:封装分表基类(解耦重复逻辑,统一管理)
水平分表的核心是动态指定表名,核心通过 setTable() 方法实现,若每个分表模型都重复编写 setTable 相关逻辑,会导致代码冗余、维护成本高。最优方案是 抽象分表基类,将通用逻辑封装,业务模型只需继承并配置少量参数即可实现分表。
适用于有时间维度等的业务(订单、账单、日志),示例:orders_202512、orders_202601
1. 步骤1:创建分表基类(支持时间分表/哈希取模分表)
// app/Models/BaseShardingModel.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Utils\ShardingUtil;
abstract class BaseShardingModel extends Model
{
// 分表基础配置(子类需重写)
// 动态表名完整如下: 表前缀_$baseTable_分表后缀
protected string $baseTable = ''; // 基础表名(如 orders)
protected string $shardingType = ''; // 分表类型:time(时间)/ hash(哈希)
protected int $hashMod = 10; // 哈希取模基数(默认10)
protected string $timeFormat = 'Ym'; // 时间分表格式(默认年月:202512)
/**
* 统一入口:获取指定分表的模型实例
* 使用方法: $orders = Order::sharding('202512')->where('user_id', 1)->get();
* 注意不能使用 Order::query()->sharding('202512') 和 DB::table('order')->sharding('202512')
*
* @param mixed $condition 分表条件(时间字符串/用户ID等)
* @return $this
*/
public static function sharding(mixed $condition): self
{
$instance = new static();
$instance->setShardingTable($condition);
return $instance;
}
/**
* 根据分表类型动态设置表名(核心逻辑)
* @param mixed $condition
* @return void
*/
protected function setShardingTable(mixed $condition): void
{
$tableName = match ($this->shardingType) {
'time' => $this->getTimeShardingTableName($condition),
'hash' => $this->getHashShardingTableName($condition),
default => throw new \InvalidArgumentException("不支持的分表类型:{$this->shardingType}"),
};
$this->setTable($tableName);
}
/**
* 生成时间分表名
* @param string $date
* @return string
*/
protected function getTimeShardingTableName(string $date): string
{
$suffix = date($this->timeFormat, strtotime($date)); // 按年月生成分表后缀
return $this->baseTable . '_' . $suffix;
}
/**
* 生成哈希取模分表名
* @param int $id
* @return string
*/
protected function getHashShardingTableName(int $id): string
{
$suffix = $id % $this->hashMod; // 按ID取模生成分表后缀
return $this->baseTable . '_' . $suffix;
}
}
2. 步骤2:业务模型继承基类(简化配置)
// 订单模型(时间分表)
// app/Models/Order.php
namespace App\Models;
class Order extends BaseShardingModel
{
protected $fillable = ['order_no', 'user_id', 'amount', 'status'];
// 仅需配置3个参数,无需重复编写分表逻辑
protected string $baseTable = 'orders';
protected string $shardingType = 'time';
protected string $timeFormat = 'Ym'; // 按年月分表
}
// 用户日志模型(哈希取模分表)
// app/Models/UserLog.php
namespace App\Models;
class UserLog extends BaseShardingModel
{
protected $fillable = ['user_id', 'content', 'operation_type'];
// 仅需配置3个参数
protected string $baseTable = 'user_logs';
protected string $shardingType = 'hash';
protected int $hashMod = 10; // 按ID%10分表
}
3. 步骤3:业务使用(极简调用)
// 时间分表:获取202512的订单模型
$order = Order::sharding('2025-12-23')->create([
'order_no' => 'ORD' . date('YmdHis') . rand(1000, 9999),
'user_id' => 1,
'amount' => 99.99,
'status' => 0
]);
// 哈希分表:获取用户1的日志模型
$log = UserLog::sharding(1)->create([
'user_id' => 1,
'content' => '优化后分表写入',
'operation_type' => 1
]);
// 查询操作同样极简
$orders = Order::sharding('202512')->where('user_id', 1)->paginate(10);
$logs = UserLog::sharding(1)->where('operation_type', 1)->orderBy('created_at', 'desc')->get();
核心优化 2:性能优化(提升分表查询 / 写入效率)
1. 优化1:严格遵循分表键查询,避免全表扫描
水平分表的核心性能原则是 「查询必须携带分表键」,避免跨分表遍历查询:
- 时间分表:查询必须指定「时间范围」(对应具体分表,如
202512),禁止无时间条件的全局查询; - 哈希分表:查询必须指定「哈希键」(如
user_id),禁止无用户ID的全局日志查询。
反例(性能极差):遍历所有订单分表查询用户1的订单
// 禁止!跨12个分表查询,性能极低
$tables = ['orders_202501', 'orders_202502', ..., 'orders_202512'];
foreach ($tables as $table) {
$orders = \DB::table($table)->where('user_id', 1)->get();
}
正例(高效查询):明确分表条件,精准定位单表
// 已知订单创建时间,直接定位对应分表
$orders = Order::sharding('202512')->where('user_id', 1)->paginate(10);
2. 优化2:批量操作优化(减少数据库连接开销)
分表场景下的批量写入/更新,优先使用 DB::table() 批量操作,避免模型循环调用:
// 优化前(性能差:循环创建,多次数据库连接)
$orderDatas = [/* 批量订单数据 */];
foreach ($orderDatas as $data) {
Order::sharding('202512')->create($data);
}
// 优化后(高性能:批量插入,单次数据库连接)
$tableName = Order::sharding('202512')->getTable();
\DB::table($tableName)->insert($orderDatas);
3. 优化3:跨分表查询优化(UnionAll + 临时表分页)
若业务必须跨分表查询(如查询用户近3个月订单),使用 unionAll 替代 union(无需去重,性能更高),并通过临时表实现分页:
// app/Services/OrderService.php
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class OrderService
{
/**
* 跨分表查询用户近N个月订单
*/
public function getUserOrdersByMonthRange(int $userId, int $monthNum = 3)
{
// 生成近N个月的分表名
$tables = [];
for ($i = 0; $i < $monthNum; $i++) {
$date = date('Y-m-01', strtotime("-$i month"));
$tableName = Order::sharding($date)->getTable();
if (DB::schema()->hasTable($tableName)) {
$tables[] = $tableName;
}
}
// 构建unionAll查询
$query = null;
foreach ($tables as $table) {
$subQuery = DB::table($table)
->select('id', 'order_no', 'user_id', 'amount', 'status', 'created_at')
->where('user_id', $userId);
$query = $query ? $query->unionAll($subQuery) : $subQuery;
}
if (!$query) {
return collect([])->paginate(10);
}
// 封装为临时表,实现分页(解决unionAll无法直接分页的问题)
$tempSql = $query->toSql();
$bindings = $query->getBindings();
return DB::table(DB::raw("({$tempSql}) as temp_orders"))
->mergeBindings($bindings)
->orderBy('created_at', 'desc')
->paginate(10);
}
}
4. 优化4:分表索引一致性保障
所有分表的索引必须与 基础表完全一致,否则会导致查询性能断崖式下降:
- 迁移文件中,批量创建分表时需同步创建索引(如哈希分表的
user_id索引、时间分表的order_no索引); - 通用建表命令中,通过
CREATE TABLE LIKE自动复制基础表索引(已在上述命令中实现); - 避免在分表中单独添加/删除索引,确保索引统一可维护。
核心优化 3:兼容 Laravel ORM 高级特性
分表模型默认支持基础ORM特性,可兼容 软删除、批量赋值、查询作用域 等高级特性
核心优化 4:自动化分表管理(避免手动操作,防止漏建表)
时间分表场景下,若手动创建下月分表,极易出现「漏建表导致业务报错」的问题。通过 Laravel 定时任务 + 通用建表命令,实现分表的自动化创建、校验,降低运维成本。
// app/Console/Commands/CreateTimeShardingTable.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use App\Models\BaseShardingModel;
use ReflectionClass;
class CreateTimeShardingTable extends Command
{
protected $signature = 'sharding:create-time-table {model : 分表模型类名(如 App\Models\Order)} {date? : 目标日期(默认下月)}';
protected $description = '通用时间分表创建命令(支持所有继承BaseShardingModel的时间分表模型)';
public function handle()
{
// 获取参数
$modelClass = $this->argument('model');
$targetDate = $this->argument('date') ?? date('Y-m-01', strtotime('+1 month'));
// 验证模型
if (!class_exists($modelClass) || !is_subclass_of($modelClass, BaseShardingModel::class)) {
$this->error("模型 {$modelClass} 不存在或未继承 BaseShardingModel");
return Command::FAILURE;
}
/** @var BaseShardingModel $model */
$model = new $modelClass();
if ($model->shardingType !== 'time') {
$this->error("模型 {$modelClass} 不是时间分表类型");
return Command::FAILURE;
}
// 获取目标表名
$tableName = $model->getTimeShardingTableName($targetDate);
if (Schema::hasTable($tableName)) {
$this->info("表 {$tableName} 已存在,无需创建");
return Command::SUCCESS;
}
// 获取基础表结构(反射获取模型对应的基础表,若不存在则报错)
$baseTable = $model->baseTable;
if (!Schema::hasTable($baseTable)) {
$this->error("基础表 {$baseTable} 不存在,无法创建分表");
return Command::FAILURE;
}
// 创建分表(复制基础表结构 + 索引)
$this->createTableWithStructure($tableName, $baseTable);
$this->info("时间分表 {$tableName} 创建成功");
return Command::SUCCESS;
}
/**
* 复制基础表结构和索引创建分表
* @param string $targetTable 目标分表名
* @param string $baseTable 基础表名
* @return void
*/
protected function createTableWithStructure(string $targetTable, string $baseTable): void
{
// 获取数据库连接
$connection = Schema::getConnection();
$driver = $connection->getDriverName();
// 不同数据库驱动的表结构复制SQL(这里以MySQL为例)
if ($driver === 'mysql') {
// 先创建空表(复制结构和索引,不复制数据)
// CREATE TABLE {$targetTable} LIKE {$baseTable} 的作用是:创建一个与「源表({$baseTable})」结构完全一致的「新表({$targetTable})」,但不复制源表中的任何数据。
$connection->statement("CREATE TABLE {$targetTable} LIKE {$baseTable}");
// 清空分表的自增ID(可选,根据业务需求)
$connection->statement("ALTER TABLE {$targetTable} AUTO_INCREMENT = 1");
return;
}
// 其他数据库驱动(PostgreSQL等)可扩展
$this->error("暂不支持 {$driver} 驱动的表结构自动复制,请手动创建");
throw new \RuntimeException("不支持的数据库驱动:{$driver}");
}
}
# 自动创建Order模型的下月分表
php artisan sharding:create-time-table App\Models\Order
# 创建Order模型202601的分表
php artisan sharding:create-time-table App\Models\Order 2026-01-01 最后更新于 2025-12-23 14:23:58 并被添加「laravel 数据库分表」标签,已有 57 位童鞋阅读过。
本站使用「署名 4.0 国际」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处
此处评论已关闭