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产品:共享数据库方案(推荐)配合严格的权限控制
- 小型应用:共享数据库,简化实现
核心原则:保证数据隔离的安全性,同时兼顾性能和可维护性。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播