mtphp 文档

分库路由 / 请求级 sticky / 脚手架增强(Phase 5)

27.1 分库(水平拆分)

框架在读写分离之外,进一步支持「分库(水平拆分)」:一张逻辑表按分片键分散到多个物理库,每个物理库在 config/database.php 的 connections 里是独立连接名。路由逻辑由 Mtphp\Database\ShardManager 承担(纯函数,仅算索引,不持有连接),上层通过 DB::shard() 取具体连接后继续链式查询。

配置(config/database.php 末尾 sharding):

'sharding' => [
    // hash:数字键按值取模、字符串键 crc32 取模,适合均分
    'orders' => [
        'shards'    => ['mysql_orders0', 'mysql_orders1', 'mysql_orders2'],
        'algorithm' => 'hash',
    ],
    // range:按单调递增边界分片(键 < 边界[i] 落分片 i,超出落最后一片)
    'logs' => [
        'shards'     => ['mysql_logs0', 'mysql_logs1'],
        'algorithm'  => 'range',
        'boundaries' => [1000000, 2000000],
    ],
],
// 同时需在 connections 中定义 mysql_orders0 / mysql_orders1 ... 等连接

使用:

// 按分片键取具体物理连接,之后与 DB::table() 完全一致
DB::shard('orders', $orderId)->table('orders')->where('id', $orderId)->get();

// 仅算连接名(便于调试/透传)
$connName = DB::shardName('orders', $orderId);

// 模型分库:声明 $shardGroup 后用 onShard()
class Order extends \Mtphp\Database\Model
{
    protected static ?string $shardGroup = 'orders';
}
Order::onShard($orderId)->where('id', $orderId)->get();   // 返回 ModelBuilder,with/orderBy 等可用

要点:

27.2 请求级 sticky(写过即读主,免显式事务)

ReadWriteConnection 原本在「事务内」强制读主以保证读己写可见。新增请求级 sticky:一旦本请求发生过写(insert/update/delete/statement),后续读也强制走主库,直到请求结束复位——这样无需把读写包进事务也能看到自己刚写的数据。

// 典型诉求:写一条,紧接着读回刚写的(无需事务也能读己写)
DB::table('user')->where('id', 1)->update(['name' => 'new']);
$row = DB::table('user')->where('id', 1)->first(); // sticky 生效,读主库,看到 'new'

27.3 脚手架命令 make:migration / make:model / make:middleware

在既有 make:seeder / make:controller 基础上补齐三个高频脚手架(均已注册进 bin/mtphp):

# 迁移:文件名按 Ymd_His_名称 生成;名称含 create_..._table 时自动推断表名
php bin/mtphp make:migration CreatePostsTable
php bin/mtphp make:migration CreatePostsTable --create=posts
php bin/mtphp make:migration AddAgeToUsersTable --table=users

# 模型:生成 Mtphp\Database\Model 子类,app/Models;表名按类名 snake_case 推导
php bin/mtphp make:model Post
php bin/mtphp make:model Api/Order          # 支持子目录

# 中间件:生成 handle(Request, callable): Response 骨架,app/Middleware
php bin/mtphp make:middleware CheckToken
php bin/mtphp make:middleware Api/AuthGate  # 支持子目录

生成的迁移为「匿名类 + up()/down()」骨架(与既有迁移约定一致),可直接 php bin/mtphp migrate 执行;模型/中间件骨架含命名空间、必要 use 与示例方法,落地即可用。

27.4 跨分片聚合查询(分库下的“查全部 / 汇总”)

分库后单条查询走 DB::shard() 即可,但「统计总数、汇总金额、取前 N」这类需要扫多片的场景,框架提供一批聚合助手:

// 1) 逐分片执行:对组内每个物理连接各跑一次回调,返回 [shardName => 结果]
//    回调里是完整的 Query(where/orderBy/limit/count… 都能用),分片若配了 read 则自动读写分离
$per = DB::across('orders', fn ($q) => $q->table('orders')->where('status', 1)->get());

// 2) 合并多片结果:把 [shard => [rows]] 压平成统一行列表(用于跨片取数后聚合展示)
$rows = DB::mergeAcross($per);

// 3) 跨片按列求和:把 [shard => [rows]] 中每行的 amount 累加(订单金额汇总等)
$sum = DB::sumAcross($per, 'amount');

// 4) 仅要各片计数再汇总(count 返回标量,直接 array_sum)
$counts = DB::across('orders', fn ($q) => $q->table('orders')->count());
$total  = array_sum($counts);

// 取出分片连接名列表(顺序即分片编号),便于自定义遍历
$names = DB::shardList('orders');

要点:

27.5 Swoole 协程级 sticky(常驻模式)

请求级 sticky(27.2)已覆盖 FPM/CLI。框架进一步自动识别 Swoole 常驻模式:sticky 状态按「协程 ID」隔离,不同协程互不污染;非协程环境自动退化为请求级(键 _fpm)。

// Swoole Server 伪代码
$server->on('request', function ($req, $res) use ($server) {
    $cid = \Swoole\Coroutine::getCid();
    \Mtphp\Database\DB::resetSticky($cid);   // 每个协程入口复位,避免“读过即读主”泄漏
    // ... 分发到框架 handle() ...
});

27.6 跨分片全局归并(排序 / 分页 / 计数)

分库下无法用单库索引做全局排序与分页。正确姿势是:先在每个分片内 orderBy + limit 收敛(利用各分片自有索引),再把多分片结果“应用层二次归并”。框架提供三个助手:

// 1) 全局计数:逐分片 count 再求和(无需全局索引)
$total = DB::countAcross('orders', 'orders');
// 可带过滤:仅统计某时间窗
$today = DB::countAcross('orders', 'orders', fn ($q) => $q->whereDate('created_at','2026-10-11'));

// 2) 全局排序(稳定排序,同值保持合并先后)
$per    = DB::across('orders', fn ($q) => $q->table('orders')->orderBy('amount','desc')->limit(100)->get());
$merged = DB::mergeAcross($per);                 // 合并多片行
$sorted = DB::sortAcross($merged, 'amount', 'DESC');

// 3) 全局分页:对排序后的跨片行切片,返回 items + 元信息
$page = DB::paginateAcross($sorted, $page = 1, $perPage = 20);
// $page = ['items'=>[...本页行...], 'total'=>总数, 'page'=>页码,
//          'perPage'=>每页条数, 'lastPage'=>末页, 'hasMore'=>是否还有下一页]

要点:

27.7 脚手架 → 迁移 真实闭环(make → migrate)

make:migration 生成的迁移遵循「匿名类 + up/down」约定,可直接由 migrate 执行,形成完整闭环:

# 1) 生成迁移(自动推断表名;--create / --table 显式指定)
php bin/mtphp make:migration CreatePostsTable --create=posts

# 2) 执行未跑过的迁移(自动建表 + 记录到 migrations 仓储)
php bin/mtphp migrate

# 3) 重复执行幂等:已跑过的迁移不会重复执行
php bin/mtphp migrate

生成的迁移文件示例(database/migrations/<时间戳>_create_posts_table.php):

<?php
declare(strict_types=1);
use Mtphp\Database\Migration;
use Mtphp\Database\Schema\Blueprint;
use Mtphp\Database\Schema\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $t): void {
            $t->id();
            // $t->string('title');
            $t->timestamps();
        });
    }
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

27.8 跨分片游标分页(keyset,避免深翻 OFFSET)

paginateAcross 用 offset 翻页,深翻时性能差且易漏读。分库下更推荐游标分页:每片各自按游标列取「> 上一页最大值」的前 N 条,合并后应用层二次归并取前 N,返回下一页游标。深翻 O(1)、天然适配分片。

// 首页(after=null,从最小 id 起);$column 建议用全局唯一且单调的列(如雪花 id),默认 id
$page = DB::cursorAcross('orders', 'orders', 'id', 20);
// $page = ['items'=>[...本页行...], 'nextCursor'=>下一页游标, 'perPage'=>20, 'count'=>本页条数]

// 翻下一页:把上一页的 nextCursor 传回即可
$next = DB::cursorAcross('orders', 'orders', 'id', 20, $page['nextCursor']);
// 末页 nextCursor 为 null,表示没有更多

27.9 脚手架「生成即可用」闭环(model / controller / middleware 真实可运行)

四个 make:* 命令生成的不是“占位空壳”,而是落地即可运行的代码,并配有端到端演示测试:

tests/MakeScaffoldE2eTest.php 用临时工程根 + 内存 SQLite 解析器,经控制台依次生成四类脚手架 → migrate 建表 → 直接实例化生成的类并调用:模型能插入并查回数据、控制器方法能返回 Response、中间件能经管道返回内部响应,证明“生成代码真实可运行”且不污染仓库。

27.10 脚手架选项联动(--resource / --migration)

脚手架命令支持常用选项,进一步减少手动补齐:

php bin/mtphp make:controller Comment --resource
php bin/mtphp make:controller Post --model=Post     # 生成直接调用 Post 模型的资源控制器
php bin/mtphp make:model Post --migration           # 等价于 -m:模型 + 建表迁移一起生成

tests/ScaffoldOptionsTest.php 验证:--resource 生成的控制器含全部 7 个动作且返回正确的 Response 与状态码;--model 生成的控制器正确 use 绑定模型、含 7 个资源动作,且经真实 migrate 建表后 index/store/show/update/destroy 全流程跑通(含删除后 404);--migration 生成的模型文件与建表迁移一一对应,跑 migrate 后表真实创建且模型可增查。

以上能力(分库路由、请求级 / 协程级 sticky、跨片聚合、跨片全局归并、跨片游标分页、四个脚手架命令、Model::onShard、make→migrate 闭环、脚手架生成即可用、脚手架选项联动)均配套零依赖单测(见 tests/ShardingTest.php、tests/StickyTest.php、tests/ShardAggTest.php、tests/ShardMergeTest.php、tests/MigrateRealTest.php、tests/MakeMigrateTest.php、tests/MakeScaffoldE2eTest.php、tests/ScaffoldOptionsTest.php),php tests/run.php 一键跑全量,全绿。