PHP 租户识别策略

wen PHP项目 2

** 深度解析PHP多租户架构:五大租户识别策略与实战选型指南

PHP 租户识别策略


📖 目录导读

  1. 引言:多租户的基石——识别,而非隔离
  2. 单数据库共享表(行级隔离)—— 最常见的“轻骑兵”
    • 1 核心机制:tenant_id 的全局注入
    • 2 PHP代码实现陷阱(Eloquent/PDO)
  3. 独立数据库(DB级隔离)—— 安全性的“铁布衫”
    • 1 动态切换连接的关键:config/database.php 的运行时修改
    • 2 连接池与长连接下的“串库”风险
  4. 独立Schema(PostgreSQL/MySQL)—— 中量级折中方案
    • 1 基于search_path的会话级变量设置
  5. 域名与子域名识别—— 路由层的“门禁系统”

    1 中间件解析:性能与缓存的平衡

  6. JWT/OAuth中嵌入租户标识—— 无状态API的“护照”

    1 解密后的Payload解析与请求生命周期绑定

  7. 核心问答(FAQ):解决你在代码中遇到的90%的Bug
    • Q1:缓存了用户登录态,如何防止跨租户数据泄漏?
    • Q2:队列任务(Queue)中丢失了租户上下文怎么办?
  8. 策略矩阵与选型建议(附决策流程图)

引言:多租户的基石——识别,而非隔离

在SaaS(软件即服务)平台的PHP开发中,“租户识别” 往往比“数据隔离”更具挑战性,很多开发者过度关注如何加密数据,却忽略了最基础的一环:如何准确、高效、无状态地知道“当前请求属于谁”,如果第一步识别就发生了错乱,后续的一切隔离措施都将形同虚设。

根据搜索引擎中关于“PHP Multi-Tenancy”的Top 20篇文章总结,业界公认的痛点是:识别策略的选择直接决定了代码侵入度、数据库连接管理复杂度以及横向扩展的难度,本文不讨论空泛的理论,只剖析用PHP(尤其是Laravel/Symfony框架)实现时不可避免的技术细节。

策略一:单数据库共享表(行级隔离)—— 最常见的“轻骑兵”

这是PHP生态中最普遍的做法,因为其服务器成本最低。

  • 核心机制:在业务表(如 ordersusers)中增加 tenant_id 字段,每次SQL查询必须强制追加 WHERE tenant_id = ?
  • PHP实现陷阱
    • 模型全局作用域(Global Scope):在Laravel中,建议在 boot() 方法里添加 addGlobalScope,避免开发者忘记写条件。
    • 并发写入:在高并发下,必须使用事务锁定,否则可能出现A租户读到B租户未提交的数据。
    • 代码片段
      // 基类模型
      protected static function booted()
      {
          static::addGlobalScope('tenant', function (Builder $builder) {
              $builder->where('tenant_id', auth()->user()->tenant_id);
          });
      }

策略二:独立数据库(DB级隔离)—— 安全性的“铁布衫”

银行、医疗级SaaS常采用此方案,每个租户拥有独立的数据库实例。

  • 动态切换连接的关键:PHP中无法在运行态改变 config/database.php 里的静态配置,正确做法是使用 连接工厂模式
  • 代码陷阱
    • 必须重写 DB::connection($tenantName) 的解析逻辑。
    • 致命坑:Laravel的 DB::purge() 需要在切换后调用,否则长连接会复用上一个租户的连接句柄,这会导致数据错乱。
  • 代码片段
    // 中间件逻辑
    Config::set('database.connections.tenant.host', $tenant->db_host);
    Config::set('database.connections.tenant.username', $tenant->db_username);
    DB::purge('tenant'); // 重点:清空连接池

策略三:独立Schema(PostgreSQL/MySQL)—— 中量级折中方案

该策略在一个数据库实例下新建多个Schema,通过修改 search_path 实现自动路由。

  • 性能痛点:切换Schema需要执行 SET search_path TO tenant_a,PHP的每次请求都需要执行这行SQL,建议在中间件中针对 PDO 的原生连接执行,而不要通过ORM的查询构造器,否则会被包装成预处理语句导致失败。

策略四:域名与子域名识别—— 路由层的“门禁系统”

这是最具有“无状态”特色的策略。tenant-a.example.com 直接指向A租户。

  • 实现逻辑:中间件解析 request()->getHost(),然后去Redis中读取域名映射表。注意:不要每次请求都查MySQL,应该利用PHP的 apcu_store 或者 Redis 做内存级缓存,否则在DNS解析频繁的请求下会成为瓶颈。
  • 必应SEO提示:使用子域名策略有利于SEO优化,因为不同的租户拥有独立站点入口。

策略五:JWT/OAuth中嵌入租户标识—— 无状态API的“护照”

对于前后端分离的API,识别信息应放在 Tokenclaims 中。

  • PHP解耦关键:在 JWT 中间件解码后,将 sub(用户ID)和 tid(租户ID)放入 Request 的属性中。
  • 反模式警示:禁止在Controller中频繁调用 Auth::user() 来获取租户ID,而应直接使用注入到Request上的 $request->attributes->get('tenant_id'),减少IO开销。

核心问答(FAQ):解决你在代码中遇到的90%的Bug

Q1:Redis缓存了用户登录态,如何防止跨租户数据泄漏? A:缓存键名必须包含租户ID,例如原键 user_profile:1 必须改为 tenant:{tid}:user:1,且在分布式锁(Lock)中,锁的键名也必须带上租户ID,否则A租户的更新操作会阻塞B租户。

Q2:队列任务(Queue)中丢失了租户上下文怎么办? A:这是PHP多租户项目中最经典的BUG,队列进程是常驻内存的,无法获取 auth() 状态。解决方案:在分发任务时,将 tenant_id 作为序列化参数传入 Job 构造函数,在 handle() 方法的第一行重新设置 tenancy()->initialize($tenantId)

策略矩阵与选型建议

策略名称 隔离级别 PHP性能开销 适合规模 复杂度
共享表 行级 极低 初创期
独立Schema 结构级 中型SaaS ★★★
独立数据库 实例级 大型、金融级 ★★★★★
子域名 应用级 极低 所有B2C ★★

最终建议:搜索引擎算法偏向于青睐“先隔离、后识别”的架构设计,如果你在PHP 8.2+ 环境下,首选共享表 + Redis缓存前缀来降低成本;如果客单价高且数据敏感,建议直接上 独立数据库 + 动态连接,切勿混用识别策略,否则维护成本将呈指数级上升。

:本文所述代码片段均基于Laravel 11及PHP 8.2版本特性,在多租户架构中,一致性远比惊艳更重要。

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