最稳妥的多租户数据库隔离方式是为每个租户分配独立数据库,动态切换PDO连接或使用连接池管理,天然避免数据混查、权限越界与DDL冲突;租户识别须在请求入口完成,连接初始化需前置,严禁跨租户复用PDO实例。

用单独数据库隔离租户最稳妥
多租户场景下,数据库层面最彻底的隔离方式就是为每个租户分配独立数据库(tenant_a_db、tenant_b_db)。PHP 连接时动态切换 PDO 实例的 dsn,或用连接池管理不同租户的连接。这种方式天然避免数据混查、权限越界、DDL 冲突等问题。
常见错误是把租户 ID 当作变量拼进 SQL——哪怕加了 WHERE tenant_id = ?,一旦漏写或参数绑定失败,立刻全库裸奔。Schema 隔离(如 PostgreSQL 的 SET search_path TO tenant_123)虽可行,但 MySQL 不支持运行时 schema 切换,且所有表必须提前建好对应 schema,运维成本陡增。
- 租户识别必须在请求入口完成(如子域名
acme.example.com→tenant_id = 'acme'),不能依赖客户端传参 - 连接复用需谨慎:一个
PDO实例不能跨租户复用,否则事务/临时表/会话变量可能污染 - 备份、迁移、审计都按库粒度操作,自动化脚本要能枚举租户库名,别硬编码
字段级隔离只适合低风险内部系统
在单库单表加 tenant_id 字段(如 users.tenant_id)是最省事的做法,但安全水位线极低。它完全依赖应用层每条 SQL 都带上 WHERE tenant_id = ?,且 ORM、原始查询、JOIN、子查询、聚合、缓存键生成等所有环节都不能漏。
典型翻车现场:SELECT * FROM orders WHERE status = 'pending' 忘加租户条件;或缓存用了 orders:pending 作为 key,结果 A 租户看到 B 租户的待处理订单;又或者管理员后台没做租户过滤,直接暴露全量数据。
立即学习“PHP免费学习笔记(深入)”;
- 必须全局启用「租户上下文」对象(如
TenantContext::current()),禁止任何地方硬编码tenant_id - 所有查询入口(Repository、DAO、Eloquent scope)强制接收
tenant_id或从上下文取,不提供无租户参数的重载方法 - 用数据库触发器或行级安全策略(PostgreSQL RLS / MySQL 8.0+ `CHECK OPTION` 视图)兜底,但别指望它替代应用逻辑
MySQL 没有真正的 schema 切换能力
有人想学 PostgreSQL 用 SET search_path 切换 schema,但在 MySQL 里 schema 就是 database 的同义词,USE tenant_123 只影响后续语句默认库,无法像 PostgreSQL 那样让同一张 users 表在不同 schema 下指向不同物理存储。强行模拟会导致表名重复、外键跨库失效、mysqldump 备份困难。
更隐蔽的坑是:MySQL 的 information_schema 查询(比如检查表是否存在)不会自动限定到当前 USE 的库,必须显式写 FROM tenant_123.users,否则查到的是别的租户的结构。
- 别在代码里写
USE xxx,改用PDO连接时指定完整dbname,或 SQL 中全限定表名(tenant_123.users) - 如果真要用多 schema,每个租户 schema 必须独立建库、独立用户、独立权限,不能共用 root 或通配符账号
- 迁移工具(如 Phinx、Laravel Migrations)需支持多库 target,否则
php artisan migrate会只刷主库
租户路由和连接初始化最容易被跳过
多数人卡在第一步:没在框架中间件或引导阶段就确定租户身份并初始化数据库连接。等到 Controller 里才去查 tenants 表反推租户,不仅慢,还可能因缓存、重定向、API Gateway 转发导致租户识别错乱。
真实线上问题常出现在子域名解析失败(api.example.com 被误判为租户)、SaaS 后台登录态未绑定租户上下文、CLI 命令(如队列消费者)根本没走 HTTP 中间件链路。
- 租户识别逻辑必须前置到 PSR-7 Request 解析后、路由匹配前,且结果要写入 request attribute 或全局 context
- 数据库连接工厂(如
ConnectionFactory)应接收tenant_id并返回专属PDO,而不是全局共享一个连接实例 - Cli 命令务必支持
--tenant=acme参数,队列 job 序列化时把tenant_id一并存进去,别靠环境变量或配置文件硬编码
租户隔离不是加个字段或切个库就完事,真正难的是所有数据访问路径都得被租户上下文穿透——包括日志、缓存、搜索索引、消息队列 payload,甚至数据库连接池的空闲连接清理策略。漏掉任意一环,都可能在某个低概率路径上把数据捅穿。











