发布于2026-07-11 阅读(0)
扫一扫,手机访问
多租户系统里,最基础也最容易被忽视的,就是索引。一个没加索引的 tenant_id,在中等规模数据下,比如单表到了50万行,一个简单的 WHERE tenant_id = ? 查询,响应时间就会从200毫秒飙升到3秒。这可不是理论推导,是线上真实踩过的坑——某次订单列表页直接卡死,查到最后,罪魁祸首就是 tenant_id 缺了索引,高并发时数据库连接池瞬间被榨干。

实操建议其实很简单,但必须严格执行:
tenant_id 的表,在迁移文件里必须显式加上索引:$table->index('tenant_id');。别想着靠ORM自动处理,那不是它的活儿。tenant_id 放在最左边:$table->index(['tenant_id', 'status', 'created_at']);。这能最大化索引效率。php artisan tinker 连进生产库,执行 DB::select("SHOW INDEX FROM orders WHERE Key_name LIKE 'tenant_id%'");,亲眼确认索引真实存在。这一步,比看十遍文档都管用。很多团队写了个 BelongsToTenant Trait,在 boot() 里调用 static::addGlobalScope(),然后就觉得万事大吉了。但谁也没想到,上线后出现了跨租户数据泄露——不是代码逻辑写错了,而是模型压根没被自动加载,全局作用域根本没挂上去。
验证方法其实很简单,但大家往往懒得做:
static::addGlobalScope('tenant', function (Builder $builder) { Log::debug('tenant scope applied'); $builder->where('tenant_id', TenantContext::id()); });App\Models\Order::all();,看日志里有没有那行 debug 输出。有,则说明作用域生效了;没有,就得赶紧排查模型加载问题。toSql() 看生成的 SQL 语句是否包含 WHERE tenant_id = ?。例如 App\Models\Order::query()->toSql(),一眼就能看出问题。EnsureTenantAccess 这个中间件,如果放到了某个子中间件之后,或者被 auth:sanctum 之类的认证中间件“提前拦截”,那后果很严重:TenantContext::set() 根本没机会执行,后续所有全局作用域都失效了。因为 TenantContext::id() 返回的是 null,WHERE tenant_id = NULL 这条过滤条件,等于没写。
常见的错误配置,我见过不少:
TenantContext::set() —— 请求生命周期已经过半,作用域注册早完成了,调了也白调。api 或 web 路由组内部,而不是整个路由文件的最顶层。正确的做法是:在 routes/web.php 开头就直接写 Route::middleware(['web', 'ensure.tenant.access'])->group(...),确保它是第一个被执行的业务中间件。这才能保证租户上下文在请求的早期就被正确设置。
只测“租户 A 能查到自己数据”,这毫无意义。真正要卡住的是“租户 A 的请求,会不会意外拿到租户 B 的数据”。这需要构造明确的跨租户断言,而不是模拟登录然后查个列表就完事。
一个最小可行的测试结构,应该包含以下四步:
Order::create(['tenant_id' => 1, 'order_no' => 'ORD-001']) 和 Order::create(['tenant_id' => 2, 'order_no' => 'ORD-001'])。tenant_id === 1 的记录,且总数恰好为 1。如果测试没覆盖这四步,那它就是个摆设。线上出问题,从来不是“查不到”,而是“查多了”——这才是最可怕的。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8