本文目录导读:

- 核心策略对比
- Database per Tenant(每个租户独立数据库)
- Schema per Tenant(每个租户独立Schema)
- Shared Database + Discriminator Column(共享表 + 租户ID列)
- 关键设计要点(无论选择哪种策略)
- 推荐选型
在 PHP 项目中实现多租户架构,核心目标是隔离不同租户的数据,确保租户 A 无法访问租户 B 的数据。
根据隔离级别和业务复杂度,主要有三种实现策略:
核心策略对比
| 策略 | 隔离性 | 成本 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Database per Tenant | 最高 | 高 | 低(代码逻辑简单) | 金融、医疗、大型企业 |
| Schema per Tenant | 中等 | 中 | 中 | 中型SaaS、需要一定隔离性 |
| Shared Database (Discriminator) | 最低 | 低 | 高(需严查SQL) | 小型SaaS、预算有限、微型租户 |
Database per Tenant(每个租户独立数据库)
这是最安全、隔离性最好的方案。
-
原理:每个租户拥有独立的MySQL/PostgreSQL数据库。
-
连接管理:根据当前租户动态切换数据库连接。
-
实现:
// 1. 租户识别(从域名、子域名或JWT中获取) $tenantId = $_SERVER['HTTP_HOST']; // 如 tenant1.example.com // 2. 租户->数据库映射(存储在配置或主表中) $tenantConfigs = [ 'tenant1.example.com' => ['host' => 'db1', 'dbname' => 'tenant_db1', ...], 'tenant2.example.com' => ['host' => 'db2', 'dbname' => 'tenant_db2', ...], ]; $config = $tenantConfigs[$tenantId]; // 3. 动态创建数据库连接(Laravel为例) Config::set('database.connections.tenant', [ 'driver' => 'mysql', 'host' => $config['host'], 'database' => $config['dbname'], 'username' => $config['username'], 'password' => $config['password'], ]); // 4. 后续所有查询使用此连接 $users = DB::connection('tenant')->table('users')->get(); -
优缺点:
- ✅ 数据完全隔离,备份恢复互不影响。
- ❌ 成本高,数据库连接数随租户增长而增长,维护多个数据库迁移脚本。
Schema per Tenant(每个租户独立Schema)
这是PostgreSQL常用的方案(MySQL的Schema等同于Database,所以通常不这样区分)。
-
原理:同一个数据库实例下,为每个租户创建独立的Schema(namespace),表名相同,但位于不同Schema下。
-
实现:在查询前,设置
search_path。SET search_path TO tenant_123, public; SELECT * FROM orders; -- 实际查询 tenant_123.orders
-
PHP实现(PostgreSQL + Doctrine DBAL):
$tenantId = getCurrentTenantId(); $connection->executeStatement("SET search_path TO tenant_{$tenantId}, public"); // 后续所有查询自动指向该Schema -
优缺点:
- ✅ 比独立数据库节省资源,连接数可控。
- ❌ 单个库性能瓶颈(所有租户共享一个数据库实例);需要DBA权限管理Schema。
Shared Database + Discriminator Column(共享表 + 租户ID列)
这是最常见且成本最低的实现方式,所有租户共用同一组数据库表。
-
原理:在每张业务表中增加
tenant_id字段,所有查询强制加上WHERE tenant_id = ?。 -
实现:
// 模型基类 / 全局作用域 (Laravel Global Scope) class TenantScope implements Scope { public function apply(Builder $builder, Model $model) { $builder->where('tenant_id', app('currentTenantId')); } } // 在模型的 booted 方法中加入 protected static function booted() { static::addGlobalScope(new TenantScope); // 创建时自动填充 tenant_id static::creating(function ($model) { $model->tenant_id = app('currentTenantId'); }); } // 查询时自动带 WHERE tenant_id=xxx $orders = Order::where('status', 'active')->get(); -
扩展:中间件注入:
// 在 Laravel 中间件中识别租户并注入全局作用域 public function handle($request, Closure $next) { $tenantId = $request->header('X-Tenant-ID') ?? extractFromSubdomain($request); app()->instance('currentTenantId', $tenantId); return $next($request); } -
优缺点:
- ✅ 部署最简单,成本最低,无需维护多库/多Schema。
- ❌ 隔离性最弱,必须防范SQL遗漏(漏掉
tenant_id就是数据泄露),索引设计需包含tenant_id以避免全租户扫描。
关键设计要点(无论选择哪种策略)
-
租户上下文:
- 在请求处理的最前端(中间件、路由守卫)识别并设置当前租户。
- 识别来源:
子域名(a.company.com)、路径前缀(/a/orders)、JWT Token、请求头(X-Tenant-ID)。
-
数据库连接池(对于独立数据库模式):
- 使用
pconnect或连接池工具(如 Swoole Hyperf、Laravel Octane)避免每个请求都创建新连接,防止数据库连接数爆炸。
- 使用
-
迁移管理(对于独立数据库/Schema):
- 使用类似
tenancy/multi-tenant或stancl/tenancy这样的Laravel扩展包,它们会自动为每个新租户运行迁移。
- 使用类似
-
数据备份与恢复:
- 独立数据库/模式:备份粒度对应单个租户,恢复容易。
- 共享表:全库备份,恢复会影响所有租户,大租户不推荐。
-
索引设计:
- 共享表模式下,联合索引必须包含
tenant_id:INDEX (tenant_id, created_at)。
- 共享表模式下,联合索引必须包含
推荐选型
- 新项目、预算有限、租户规模小(<1000): Shared Table + Discriminator。
- 中大型SaaS、已有独立数据库经验、租户集中度高: Database per Tenant。
- 使用PostgreSQL、需要平衡成本与隔离性: Schema per Tenant。
对于大多数PHP框架(Laravel、Symfony),推荐使用 Shared Table (Discriminator) 方案起步,利用框架的全局作用域(Global Scope)或Eloquent模型事件自动注入租户ID,可以较低成本实现可靠的隔离,切换成本在后期也相对可控(可以将数据迁移到独立数据库)。