1. PHP多租户数据隔离方案概述
在SaaS应用开发中,多租户架构是实现资源高效利用的关键设计模式。PHP作为Web开发的主流语言之一,其多租户实现需要解决的核心问题是:如何在单一应用实例中为不同租户提供逻辑隔离的数据存储与访问。
我经历过三个大型SaaS项目的完整生命周期,从早期的简单字段过滤到成熟的Schema隔离方案,踩过不少数据交叉泄露的坑。最惊险的一次是某医疗系统因租户ID传参缺失导致患者数据错乱,最终通过数据库事务回滚+操作日志追溯才挽回损失。这些教训让我深刻认识到:多租户不是简单的SQL拼接游戏,而是需要体系化的隔离设计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多租户核心隔离方案对比
2.1 共享数据库独立Schema
sql复制-- 租户A的订单表
CREATE TABLE tenant_a.orders (
id INT PRIMARY KEY,
user_id INT NOT NULL
);
-- 租户B的订单表
CREATE TABLE tenant_b.orders (
id INT PRIMARY KEY,
user_id INT NOT NULL
);
优势:
- 物理隔离级别最高,不同租户的表结构可定制
- 备份恢复可针对单个租户操作
- 数据库连接池复用率高
劣势:
- 数据库连接字符串需要动态切换
- 跨租户统计查询复杂
- 需要预先分配Schema资源
关键技巧:用Laravel的
Schema::connection()实现动态切换,配合中间件自动设置当前租户上下文
2.2 共享表增加租户字段
php复制// 带tenant_id的模型基类
abstract class TenantModel extends Model {
protected static function boot() {
parent::boot();
static::addGlobalScope(new TenantScope);
}
}
class TenantScope implements Scope {
public function apply(Builder $builder, Model $model) {
$builder->where($model->getTable().'.tenant_id', Auth::tenantId());
}
}
性能优化点:
- 必须为tenant_id字段建立索引
- 复合索引应将tenant_id作为首列
- 使用Redis缓存租户路由映射表
2.3 独立数据库实例
适用于金融、医疗等合规要求严格的场景。我曾为某医保系统设计过这样的架构:
code复制负载均衡层
├── 租户A集群
│ ├── app01.example.com
│ └── db01.internal
└── 租户B集群
├── app02.example.com
└── db02.internal
实施要点:
- 通过DNS子域名区分租户
- 数据库连接信息存储在配置中心
- 需要实现跨实例的数据同步工具
3. Laravel多租户实战
3.1 租户识别中间件
php复制class IdentifyTenant {
public function handle($request, $next) {
$tenant = $this->resolveTenant(
$request->header('X-Tenant'),
$request->route('tenant')
);
if (!$tenant) abort(403);
Tenant::setCurrent($tenant);
return $next($request);
}
protected function resolveTenant($header, $routeParam) {
// 优先从JWT令牌解析
if ($jwt = $header) {
return Tenant::where('key', $jwt)->first();
}
// 次选子域名识别
$subdomain = explode('.', $request->getHost())[0];
return Tenant::where('domain', $subdomain)->first();
}
}
3.2 数据库连接管理
php复制// 动态创建连接
config(['database.connections.tenant' => [
'driver' => 'mysql',
'host' => $tenant->db_host,
'database' => $tenant->db_name,
'username' => decrypt($tenant->db_user),
'password' => decrypt($tenant->db_pass)
]]);
DB::purge('tenant');
DB::reconnect('tenant');
3.3 队列任务隔离
php复制// 任务基类属性
protected $tenantId;
public function __construct() {
$this->tenantId = Tenant::current()->id;
}
public function handle() {
Tenant::setById($this->tenantId);
// 业务逻辑...
}
4. 性能优化策略
4.1 连接池管理
使用ProxySQL实现连接复用:
ini复制# proxies.cnf
INSERT INTO mysql_servers VALUES(1,'master-tenant',3306);
INSERT INTO mysql_users VALUES('proxy_user','password');
# 按租户分组的连接池
INSERT INTO mysql_servers VALUES(2,'tenant_pool_a',3306);
4.2 缓存隔离
Redis键名设计规范:
code复制tenant:{id}:cache_key
使用Predis管道批量清除租户缓存:
php复制$keys = $redis->keys("tenant:{$id}:*");
$redis->pipeline(function($pipe) use ($keys) {
foreach ($keys as $key) {
$pipe->del($key);
}
});
4.3 文件存储隔离
对象存储路径规范:
code复制/tenants/{id}/uploads/year/month/file.ext
使用Flysystem多磁盘配置:
php复制'disks' => [
'tenant_assets' => [
'driver' => 's3',
'root' => 'tenants/'.Tenant::id()
]
]
5. 安全防护要点
5.1 租户数据越权
典型漏洞:
php复制// 错误示例:未验证订单所属租户
$order = Order::find($id);
$order->update($request->all());
修复方案:
php复制$order = Order::where('id', $id)
->where('tenant_id', Tenant::id())
->firstOrFail();
5.2 批量操作防护
限制跨租户的SQL注入风险:
php复制// 禁止直接执行租户传入的SQL
DB::statement("UPDATE orders SET status=1 WHERE id IN ($ids)");
// 应使用参数化查询
DB::table('orders')
->whereIn('id', explode(',', $ids))
->where('tenant_id', Tenant::id())
->update(['status' => 1]);
5.3 审计日志规范
php复制class AuditLogger {
public static function log($action, $model = null) {
DB::table('audits')->insert([
'tenant_id' => Tenant::id(),
'user_id' => auth()->id(),
'action' => $action,
'model_type' => $model ? get_class($model) : null,
'model_id' => $model ? $model->id : null,
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'created_at' => now()
]);
}
}
6. 踩坑实录与解决方案
6.1 租户上下文丢失
现象:队列任务中无法获取当前租户
根因:序列化任务时未保存租户状态
修复:
php复制// 在Job基类中
protected $tenantId;
public function __construct() {
$this->tenantId = Tenant::id();
}
public function handle() {
Tenant::setById($this->tenantId);
// ...
}
6.2 Nginx静态资源缓存
问题:不同租户的CSS/JS文件被共用
配置方案:
nginx复制location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public";
add_header Vary "X-Tenant"; # 按租户头区分缓存
}
6.3 跨租户数据迁移
安全迁移步骤:
- 在目标租户数据库创建临时表
- 使用openssl加密导出文件
- 通过SFTP传输加密文件
- 目标端解密后导入
- 校验数据完整性哈希值
bash复制# 导出时加密
mysqldump -u user -p db table | openssl enc -aes-256-cbc -out table.sql.enc
# 导入时解密
openssl enc -d -aes-256-cbc -in table.sql.enc | mysql -u user -p target_db
7. 监控与维护
7.1 关键指标监控
Grafana看板应包含:
- 各租户API响应时间P99
- 数据库连接数按租户分布
- 存储空间使用增长趋势
- 异常登录尝试次数
7.2 自动化测试策略
PHPUnit测试套件设计:
php复制class MultiTenantTestCase extends TestCase {
protected $tenants = [];
protected function setUp(): void {
parent::setUp();
$this->tenants[] = Tenant::create([/* ... */]);
Tenant::setCurrent($this->tenants[0]);
}
public function testCrossTenantAccess() {
$data = Model::first()->toArray();
Tenant::setCurrent($this->tenants[1]);
$this->getJson("/api/data/{$data['id']}")
->assertStatus(403);
}
}
7.3 备份恢复流程
多租户备份策略矩阵:
| 租户等级 | 备份频率 | 保留周期 | 存储位置 |
|---|---|---|---|
| VIP | 每小时 | 30天 | 异地双活 |
| 标准 | 每日 | 7天 | 同城备份 |
| 试用 | 每周 | 1天 | 本地磁盘 |
恢复检查清单:
- 验证租户数据库字符集
- 检查外键约束完整性
- 重建搜索引擎索引
- 发送恢复完成通知邮件
