PHP项目如何实现多租户架构?

wen PHP项目 3

本文目录导读:

PHP项目如何实现多租户架构?

  1. 核心策略对比
  2. Database per Tenant(每个租户独立数据库)
  3. Schema per Tenant(每个租户独立Schema)
  4. Shared Database + Discriminator Column(共享表 + 租户ID列)
  5. 关键设计要点(无论选择哪种策略)
  6. 推荐选型

在 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 以避免全租户扫描。

关键设计要点(无论选择哪种策略)

  1. 租户上下文

    • 在请求处理的最前端(中间件、路由守卫)识别并设置当前租户。
    • 识别来源:子域名a.company.com)、路径前缀/a/orders)、JWT Token请求头X-Tenant-ID)。
  2. 数据库连接池(对于独立数据库模式):

    • 使用 pconnect 或连接池工具(如 Swoole Hyperf、Laravel Octane)避免每个请求都创建新连接,防止数据库连接数爆炸。
  3. 迁移管理(对于独立数据库/Schema):

    • 使用类似 tenancy/multi-tenantstancl/tenancy 这样的Laravel扩展包,它们会自动为每个新租户运行迁移。
  4. 数据备份与恢复

    • 独立数据库/模式:备份粒度对应单个租户,恢复容易。
    • 共享表:全库备份,恢复会影响所有租户,大租户不推荐。
  5. 索引设计

    • 共享表模式下,联合索引必须包含 tenant_idINDEX (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,可以较低成本实现可靠的隔离,切换成本在后期也相对可控(可以将数据迁移到独立数据库)。

抱歉,评论功能暂时关闭!