PHP 多租户方案设计
多租户架构是指一个应用实例同时为多个租户(客户/组织)提供服务,每个租户拥有独立的数据和配置,本文将探讨PHP实现多租户的三种主要方案。

架构方案对比
独立数据库
每个租户拥有独立的数据库。
优点:
- 数据隔离性最强
- 便于单独备份和恢复
- 可根据租户需求定制功能
缺点:
- 成本高(数据库连接多)
- 维护复杂(迁移、升级需同步)
- 连接管理开销大
共享数据库,独立Schema
每个租户拥有独立的表空间(Schema)。
优点:
- 数据隔离性较好
- 便于数据迁移
- 共享部分连接资源
缺点:
- 维护成本较高
- Schema数量过多时管理困难
共享数据库,共享Schema,共享表
所有租户共用同一套表,通过租户ID区分。
优点:
- 成本最低
- 维护简单
- 扩展性好
缺点:
- 数据隔离性较弱
- 需要严格的权限控制
方案三详细设计(推荐)
1 数据库设计
-- 租户表
CREATE TABLE tenants (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
domain VARCHAR(100) UNIQUE,
status TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 业务数据表(带租户ID)
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
order_no VARCHAR(50) NOT NULL,
amount DECIMAL(10,2),
status TINYINT,
created_at TIMESTAMP,
INDEX idx_tenant_id (tenant_id),
UNIQUE KEY uk_order_tenant (tenant_id, order_no)
);
2 租户识别与配置
<?php
class TenantContext
{
private static $tenantId;
private static $tenantConfig;
public static function setTenant($tenantId)
{
self::$tenantId = $tenantId;
}
public static function getTenantId()
{
return self::$tenantId;
}
public static function getTenantConfig()
{
return self::$tenantConfig;
}
}
// 租户识别逻辑
class TenantResolver
{
public static function resolve(Request $request)
{
// 1. 域名识别
$domain = $request->getHost();
// 2. 子域名识别
$subdomain = explode('.', $domain)[0];
// 3. URL路径识别
$path = $request->getPathInfo();
// 4. Header/Token识别
$tenantId = $request->headers->get('X-Tenant-Id');
// 5. 从数据库获取租户信息
$tenant = Tenant::findByDomain($domain);
if ($tenant) {
TenantContext::setTenant($tenant->id);
return true;
}
return false;
}
}
3 数据库连接管理
<?php
class DatabaseManager
{
private static $connections = [];
public static function getConnection($tenantId = null)
{
$tenantId = $tenantId ?: TenantContext::getTenantId();
$config = self::getTenantDatabaseConfig($tenantId);
$key = md5($tenantId . json_encode($config));
if (!isset(self::$connections[$key])) {
self::$connections[$key] = new PDO(
"mysql:host={$config['host']};dbname={$config['database']}",
$config['username'],
$config['password']
);
}
return self::$connections[$key];
}
private static function getTenantDatabaseConfig($tenantId)
{
// 方案一:所有租户共享配置
$baseConfig = [
'host' => getenv('DB_HOST'),
'database' => getenv('DB_DATABASE'),
'username' => getenv('DB_USERNAME'),
'password' => getenv('DB_PASSWORD')
];
// 方案二:根据租户获取独立配置
if (getenv('TENANT_DB_MODE') === 'separate') {
$tenant = Tenant::find($tenantId);
return [
'host' => $tenant->db_host,
'database' => $tenant->db_name,
'username' => $tenant->db_user,
'password' => $tenant->db_password
];
}
return $baseConfig;
}
}
4 数据访问层封装
<?php
abstract class BaseModel
{
protected $table;
protected $connection;
protected $tenantScoped = true;
public function __construct()
{
$this->connection = DatabaseManager::getConnection();
}
protected function applyTenantScope($query)
{
if ($this->tenantScoped) {
$query->where('tenant_id', TenantContext::getTenantId());
}
return $query;
}
public function findAll()
{
$sql = "SELECT * FROM {$this->table} WHERE tenant_id = ?";
$stmt = $this->connection->prepare($sql);
$stmt->execute([TenantContext::getTenantId()]);
return $stmt->fetchAll();
}
public function create($data)
{
$data['tenant_id'] = TenantContext::getTenantId();
// 插入逻辑...
}
public function delete($id)
{
$sql = "DELETE FROM {$this->table} WHERE id = ? AND tenant_id = ?";
$stmt = $this->connection->prepare($sql);
$stmt->execute([$id, TenantContext::getTenantId()]);
}
}
class Order extends BaseModel
{
protected $table = 'orders';
public function getOrderByNumber($orderNo)
{
$sql = "SELECT * FROM {$this->table}
WHERE tenant_id = ? AND order_no = ?";
$stmt = $this->connection->prepare($sql);
$stmt->execute([TenantContext::getTenantId(), $orderNo]);
return $stmt->fetch();
}
}
5 中间件设计
<?php
class TenantMiddleware
{
public function handle($request, Closure $next)
{
// 排除公共路由
if ($this->isPublicRoute($request)) {
return $next($request);
}
// 识别租户
if (!TenantResolver::resolve($request)) {
return response()->json(['error' => 'Invalid tenant'], 401);
}
// 设置数据访问范围
$this->setQueryScopes();
// 注入租户上下文
app()->instance('tenant', TenantContext::getTenant());
return $next($request);
}
private function setQueryScopes()
{
// 设置全局查询作用域
Eloquent::addGlobalScope('tenant', function ($builder) {
$builder->where('tenant_id', TenantContext::getTenantId());
});
}
}
6 缓存隔离
<?php
class TenantCache
{
private static $prefix = 'tenant:';
public static function getKey($key)
{
return self::$prefix . TenantContext::getTenantId() . ':' . $key;
}
public static function get($key)
{
return Cache::get(self::getKey($key));
}
public static function put($key, $value, $ttl = 3600)
{
return Cache::put(self::getKey($key), $value, $ttl);
}
public static function flushTenant()
{
$prefix = self::$prefix . TenantContext::getTenantId();
return Cache::flush($prefix);
}
}
7 文件存储隔离
<?php
class TenantStorage
{
public static function getTenantPath()
{
$tenantId = TenantContext::getTenantId();
return "uploads/tenant_{$tenantId}/";
}
public static function upload($file, $path = '')
{
$tenantPath = self::getTenantPath() . $path;
// 使用数据库存储(可选)
$cloudPath = Storage::cloud()->putFile($tenantPath, $file);
// 保存文件记录到数据库
FileRecord::create([
'tenant_id' => TenantContext::getTenantId(),
'path' => $cloudPath,
'name' => $file->getClientOriginalName()
]);
return $cloudPath;
}
}
8 权限控制
<?php
class TenantPermissionMiddleware
{
public function handle($request, Closure $next, $permission)
{
$tenant = TenantContext::getTenant();
// 检查租户权限
if (!$tenant->hasPermission($permission)) {
abort(403, 'Tenant does not have permission');
}
// 检查用户权限
$user = auth()->user();
if (!$user->hasPermission($permission)) {
abort(403, 'User does not have permission');
}
return $next($request);
}
}
性能优化
1 索引优化
-- 联合索引 CREATE INDEX idx_tenant_order_date ON orders (tenant_id, created_at); -- 唯一约束 CREATE UNIQUE INDEX uk_tenant_order_no ON orders (tenant_id, order_no);
2 分区分表
-- 按租户ID分区分表
CREATE TABLE orders_tenant_1 PARTITION BY HASH(tenant_id) PARTITIONS 10;
-- 或按时间分区
CREATE TABLE logs (
id BIGINT,
tenant_id BIGINT,
log_time DATETIME
) PARTITION BY RANGE (YEAR(log_time)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);
3 缓存策略
<?php
class TenantCacheStrategy
{
private static $strategies = [
// 数据容易变化,低缓存
'inventory' => 60,
// 数据变化慢,高缓存
'settings' => 3600,
// 静态数据,永久缓存
'country_codes' => 86400
];
public static function getCacheTime($key)
{
return self::$strategies[$key] ?? 300;
}
}
安全考虑
1 数据隔离验证
<?php
class TenantSecurity
{
public static function validateTenantBoundary()
{
// 开发环境使用
app()->after(function ($request, $response) {
// 检查响应数据中是否包含非当前租户数据
$tenantId = TenantContext::getTenantId();
// 记录潜在风险查询
$queries = DB::getQueryLog();
foreach ($queries as $query) {
if (!str_contains($query['query'], "tenant_id = {$tenantId}")
&& !str_contains($query['query'], 'tenants')) {
logger()->warning('Potential tenant data leak: ' . $query['query']);
}
}
});
}
}
2 SQL注入防护
<?php
class SafeQueryBuilder
{
public static function build($table, $conditions)
{
$sql = "SELECT * FROM {$table} WHERE tenant_id = :tenant_id";
$params = ['tenant_id' => TenantContext::getTenantId()];
foreach ($conditions as $field => $value) {
if (!self::isWhitelistedField($field)) {
continue;
}
$sql .= " AND {$field} = :{$field}";
$params[$field] = $value;
}
return [$sql, $params];
}
private static function isWhitelistedField($field)
{
$allowed = ['id', 'status', 'order_no', 'amount'];
return in_array($field, $allowed);
}
}
运维与监控
1 数据备份策略
<?php
class BackupManager
{
public static function backupTenant($tenantId)
{
// 获取租户所有数据
$tables = ['orders', 'users', 'products', 'settings'];
// 导出为JSON/CSV
foreach ($tables as $table) {
$data = DB::table($table)
->where('tenant_id', $tenantId)
->get();
// 压缩存储
$content = json_encode($data);
Storage::put("backups/tenant_{$tenantId}/{$table}.json", $content);
}
// 通知租户
event(new TenantBackupCompleted($tenantId));
}
public static function restoreTenant($tenantId, $backupPath)
{
// 恢复逻辑...
}
}
2 租户指标监控
<?php
class TenantMetrics
{
public static function collect()
{
$tenantId = TenantContext::getTenantId();
$metrics = [
'requests' => self::getRequestCount($tenantId),
'storage_usage' => self::getStorageUsage($tenantId),
'api_calls' => self::getApiCallCount($tenantId),
'errors' => self::getErrorCount($tenantId)
];
// 存储指标
Metrics::create([
'tenant_id' => $tenantId,
'data' => $metrics,
'recorded_at' => now()
]);
}
public static function detectAnomalies($tenantId)
{
// 异常检测逻辑
$metrics = self::getRecentMetrics($tenantId);
if ($metrics['requests'] > self::threshold('requests')) {
event(new HighTrafficAlert($tenantId));
}
}
}
部署与扩展
1 负载均衡
# Nginx 配置示例
upstream app_servers {
least_conn;
server 10.0.0.1:80 max_fails=3 fail_timeout=30s;
server 10.0.0.2:80 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_servers;
proxy_set_header X-Tenant-Id $http_x_tenant_id;
}
}
2 水平扩展
# Docker Compose 多实例部署
version: '3'
services:
app:
image: your-app:latest
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
volumes:
- ./storage:/var/www/storage
ports:
- "8080:80"
depends_on:
- mysql
- redis
app2:
image: your-app:latest
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
volumes:
- ./storage2:/var/www/storage
ports:
- "8081:80"
最佳实践建议
- 数据隔离优先:即使使用共享数据库,也要在代码层强制实施租户隔离
- 缓存键加租户前缀:避免缓存数据泄露
- 监控租户使用量:为计费和资源管理提供依据
- 提供租户自助服务:注册、配置、数据导出等
- 定期数据清理:涉及租户数据保留策略
- 测试租户边界:编写专门的测试用例验证数据隔离
PHP多租户方案设计需要根据业务需求、预算和技术栈选择合适方案:
- 大型企业:独立数据库或Schema方案
- SaaS产品:共享数据库方案(推荐)配合严格的权限控制
- 小型应用:共享数据库,简化实现
核心原则:保证数据隔离的安全性,同时兼顾性能和可维护性。