Yii 2 多租户必须从数据库隔离粒度、请求生命周期控制点、缓存穿透防护三层面协同设计:独立数据库需重置db组件而非仅改DSN,共享库行级隔离须覆盖所有查询入口,缓存key必须含tenant_id且schema缓存按租户隔离。

不能靠“加机器”硬扛,得从数据库隔离粒度、请求生命周期控制点、缓存穿透防护三个层面同时下刀——否则用户量一过百万,tenant_id 漏写一处,全库数据就裸奔。
独立数据库方案必须重置 db 组件,不是改 DSN 就完事
很多人以为只要动态替换 Yii::$app->db->dsn 就能切库,实际只是改了配置字符串,底层连接对象还是旧的。事务、查询缓存、连接池都会跨租户污染。
- 必须在
beforeRequest事件中调用Yii::$app->setComponents(['db' => $newDbConfig]),强制重建组件实例 -
Yii::$app->db->open()要显式执行,否则首次查询时才 lazy-open,中间件(如 RBAC、日志)可能已走错库 - 迁移脚本不能复用全局
./yii migrate,必须带--db=db_tenant_123参数逐个运行 - PostgreSQL 下 schema 切换成本低,MySQL 必须建 N 个库,运维压力陡增;千万级用户若全走此路,DB 实例数会突破运维阈值
共享库 + 行级隔离必须覆盖所有查询入口,漏一个就崩
这是最易低估复杂度的方案。Yii 2 不提供开箱即用的租户拦截,ActiveRecord::find()、createCommand()、关联查询 with()、原生 SQL 都是独立入口,各自绕过全局过滤。
- 基类
BaseActiveRecord的find()方法必须重写,统一加andWhere(['tenant_id' => Yii::$app->tenant->id]) - 禁止直接 new
\yii\db\Command,要封装tenantCommand()工厂,自动注入tenant_id条件 -
with()关联默认不继承主模型条件,关系定义里得显式写'on' => ['tenant_id' => new \yii\db\Expression('[[tenant_id]]')] - Gii 生成的代码、第三方扩展(如
yii2-admin)默认不兼容,必须手动补丁或替换行为
缓存与 schema 缓存必须按租户隔离,否则 key 冲突雪崩
Yii 默认的 schemaCache 是全局共享的,如果多个租户共用同一套表结构但不同库,缓存会互相覆盖,导致字段元信息错乱;Redis 缓存若没加租户前缀,A 租户查到 B 租户的用户头像都可能。
- 数据库 schema 缓存必须绑定租户:把
schemaCache改成tenantSchemaCache组件,并在切换 DB 后同步切换该组件 - 所有缓存 key 必须含
tenant_id,例如user_profile_{$tenantId}_{$userId},不能只用user_profile_{$userId} -
enableSchemaCache开启后,schemaCacheDuration建议设为3600(1小时),避免租户建表后 schema 缓存长期不更新 - Redis 连接池需按租户分组,或至少用不同 database ID(如
database => $tenantId % 16),防 key 冲突
真正卡住千万级落地的,从来不是 PHP 性能,而是租户上下文在框架各层的传递一致性——从路由识别、DB 切换、缓存 key 构造,到日志埋点、异常上报,每个环节都要确认 Yii::$app->tenant->id 是否真实可用且未被覆盖。任何一层脱钩,都会让隔离性归零。











