Laravel 10 水平分表优化

Laravel10水平分表优化核心: 解耦、自动化、高性能、可监控

  • 封装 BaseShardingModel 基类,消除重复代码,统一分表逻辑,业务模型极简接入;
  • 实现通用定时任务 + 建表命令,自动化创建分表,避免手动操作失误;
  • 遵循分表查询原则,使用 unionAll 优化跨分表查询,批量操作提升写入效率;
  • 兼容 Laravel ORM 高级特性(软删除、查询作用域),不改变原有开发习惯;
  • 增加分表监控与批量备份,保障生产环境的稳定性和数据安全性。

核心优化 1:封装分表基类(解耦重复逻辑,统一管理)

水平分表的核心是动态指定表名,核心通过 setTable() 方法实现,若每个分表模型都重复编写 setTable 相关逻辑,会导致代码冗余、维护成本高。最优方案是 抽象分表基类,将通用逻辑封装,业务模型只需继承并配置少量参数即可实现分表。

适用于有时间维度等的业务(订单、账单、日志),示例:orders_202512orders_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

此处评论已关闭