一、多租户架构概述
在当今云计算时代,SaaS(软件即服务)模式已成为企业服务的主流。多租户架构允许单个应用实例为多个客户(租户)提供服务,同时确保数据隔离和安全性。本文将深入探讨如何使用ThinkPHP 6.0构建健壮的多租户系统。
系统架构设计图
用户请求 → 路由解析 → 租户识别 → 数据库连接切换 → 业务处理
↑ ↓ ↓ ↓ ↓
响应返回 ← 数据封装 ← 中间件处理 ← 上下文设置 ← 服务调用
二、核心实现方案
2.1 基于子域名的租户识别方案
通过子域名识别租户是最常见的方案,例如:tenant1.app.com、tenant2.app.com
路由配置(route/app.php)
// 动态子域名路由
Route::domain(':tenant', function() {
// 租户专属路由组
Route::get('dashboard', 'Tenant/Dashboard/index');
Route::resource('products', 'Tenant/Product');
})->pattern(['tenant' => '[a-z0-9-]+']);
2.2 租户上下文中间件
<?php
namespace appmiddleware;
class TenantContext
{
public function handle($request, Closure $next)
{
// 从子域名获取租户标识
$tenantId = $request->subDomain();
// 验证租户有效性
$tenant = appmodelTenant::where('identifier', $tenantId)
->where('status', 1)
->find();
if (!$tenant) {
return json(['code' => 404, 'msg' => '租户不存在']);
}
// 设置全局租户上下文
app()->bind('tenant', $tenant);
// 动态切换数据库连接
$this->switchDatabase($tenant);
return $next($request);
}
private function switchDatabase($tenant)
{
// 配置独立的数据库连接
config()->set([
'database.connections.tenant' => [
'type' => 'mysql',
'hostname' => env('database.hostname'),
'database' => 'saas_' . $tenant->db_suffix,
'username' => env('database.username'),
'password' => env('database.password'),
'charset' => 'utf8mb4',
'prefix' => 't_',
]
]);
// 设置当前租户的数据库连接
app()->db->connect('tenant');
}
}
三、数据隔离策略实现
3.1 数据库级别隔离
每个租户拥有独立的数据库,提供最高级别的数据安全隔离。
数据库迁移工具扩展
<?php
namespace appcommand;
use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;
class TenantMigrate extends Command
{
protected function configure()
{
$this->setName('tenant:migrate')
->setDescription('为所有租户执行数据库迁移');
}
protected function execute(Input $input, Output $output)
{
$tenants = appmodelTenant::select();
foreach ($tenants as $tenant) {
$output->writeln("正在为租户 {$tenant->name} 执行迁移...");
// 动态切换数据库配置
$this->setTenantDatabase($tenant);
// 执行迁移命令
$this->runMigration();
$output->writeln("租户 {$tenant->name} 迁移完成");
}
}
private function setTenantDatabase($tenant)
{
// 动态修改数据库配置
config()->set('database.connections.tenant.database',
'saas_' . $tenant->db_suffix);
}
}
3.2 模型层自动作用域
<?php
namespace appmodel;
use thinkModel;
class BaseTenantModel extends Model
{
protected $connection = 'tenant';
// 自动添加租户ID过滤
protected $globalScope = ['tenant'];
/**
* 定义全局作用域
*/
public function scopeTenant($query)
{
$tenant = app()->tenant;
if ($tenant && $this->hasField('tenant_id')) {
$query->where('tenant_id', $tenant->id);
}
}
/**
* 自动写入租户ID
*/
protected static function onBeforeInsert($model)
{
$tenant = app()->tenant;
if ($tenant && $model->hasField('tenant_id')) {
$model->tenant_id = $tenant->id;
}
}
}
// 具体业务模型继承
class Product extends BaseTenantModel
{
protected $table = 'products';
// 所有查询自动添加 tenant_id 条件
// 所有插入自动设置 tenant_id
}
四、高级功能实现
4.1 租户自定义配置系统
<?php
namespace appservice;
class TenantConfig
{
protected $cacheKey = 'tenant_config:%s';
/**
* 获取租户配置
*/
public function get($key, $default = null)
{
$tenant = app()->tenant;
$cacheKey = sprintf($this->cacheKey, $tenant->id);
// 从缓存获取配置
$config = cache($cacheKey);
if (!$config) {
$config = $this->loadConfigFromDB($tenant);
cache($cacheKey, $config, 3600);
}
return $config[$key] ?? $default;
}
/**
* 动态配置应用
*/
public function applyConfig()
{
// 应用主题配置
$theme = $this->get('theme', 'default');
config()->set('template.theme', $theme);
// 应用业务配置
$settings = $this->get('business_settings', []);
config()->set('tenant_settings', $settings);
}
}
4.2 跨租户数据统计(管理员功能)
<?php
namespace appadminservice;
class CrossTenantAnalytics
{
/**
* 获取所有租户的统计数据
*/
public function getPlatformStatistics($startDate, $endDate)
{
$tenants = appmodelTenant::select();
$statistics = [];
foreach ($tenants as $tenant) {
// 切换到租户数据库
$this->switchToTenant($tenant);
// 执行统计查询
$stats = [
'tenant_name' => $tenant->name,
'user_count' => appmodelUser::count(),
'order_count' => appmodelOrder::whereTime('create_time',
'between', [$startDate, $endDate])->count(),
'revenue' => appmodelOrder::whereTime('create_time',
'between', [$startDate, $endDate])->sum('amount')
];
$statistics[] = $stats;
}
return $statistics;
}
private function switchToTenant($tenant)
{
// 动态修改数据库连接
config()->set([
'database.connections.tenant.database' => 'saas_' . $tenant->db_suffix
]);
app()->db->connect('tenant', true);
}
}
五、性能优化策略
5.1 连接池管理
<?php
namespace apppool;
use SwooleDatabasePDOPool;
use thinkfacadeConfig;
class TenantConnectionPool
{
protected static $pools = [];
/**
* 获取租户数据库连接池
*/
public static function getPool($tenantId)
{
if (!isset(self::$pools[$tenantId])) {
$config = self::getTenantDbConfig($tenantId);
self::$pools[$tenantId] = new PDOPool(
$config,
Config::get('database.pool.max_connections', 20),
Config::get('database.pool.timeout', 5.0)
);
}
return self::$pools[$tenantId];
}
/**
* 协程安全的连接获取
*/
public static function getConnection($tenantId)
{
$pool = self::getPool($tenantId);
return SwooleCoroutine::getContext()[$tenantId] ??
(SwooleCoroutine::getContext()[$tenantId] = $pool->get());
}
}
5.2 多级缓存策略
<?php
namespace appcache;
class TenantCache
{
protected $prefix = 'tenant_cache:';
protected $tenantId;
public function __construct($tenantId)
{
$this->tenantId = $tenantId;
}
/**
* 租户感知的缓存键
*/
protected function buildKey($key)
{
return $this->prefix . $this->tenantId . ':' . $key;
}
/**
* 智能缓存获取
*/
public function remember($key, $callback, $ttl = 3600)
{
$cacheKey = $this->buildKey($key);
// 尝试从本地缓存获取
$localCache = app()->get('local_cache');
if ($localCache && $localCache->has($cacheKey)) {
return $localCache->get($cacheKey);
}
// 尝试从Redis获取
$value = cache($cacheKey);
if (!$value) {
$value = $callback();
cache($cacheKey, $value, $ttl);
// 设置本地缓存
if ($localCache) {
$localCache->set($cacheKey, $value, min($ttl, 60));
}
}
return $value;
}
}
六、部署与监控
6.1 Docker多租户部署配置
docker-compose.yml 配置
version: '3.8'
services:
saas-app:
build: .
environment:
- APP_DEBUG=false
- TENANT_MODE=multi
volumes:
- ./runtime:/app/runtime
- ./tenant-configs:/app/config/tenants
depends_on:
- redis
- mysql-master
mysql-master:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: saas_platform
volumes:
- mysql-data:/var/lib/mysql
- ./init-tenants.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:6-alpine
command: redis-server --appendonly yes
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/tenants.conf:/etc/nginx/conf.d/default.conf
- ./public:/app/public
volumes:
mysql-data:
tenant-configs:
6.2 租户系统监控
<?php
namespace appmonitor;
use thinkfacadeLog;
class TenantMonitor
{
/**
* 记录租户操作日志
*/
public static function logOperation($action, $data = [])
{
$tenant = app()->tenant;
$context = [
'tenant_id' => $tenant->id,
'tenant_name' => $tenant->name,
'user_id' => session('user_id'),
'action' => $action,
'ip' => request()->ip(),
'user_agent' => request()->header('user-agent'),
'timestamp' => time(),
'data' => $data
];
// 写入日志文件
Log::write(json_encode($context), 'tenant_operations');
// 发送到监控系统(可选)
if (config('app.monitor_enabled')) {
self::sendToMonitor($context);
}
}
/**
* 租户资源使用统计
*/
public static function collectMetrics()
{
$tenant = app()->tenant;
$metrics = [
'db_queries' => app()->db->getQueryTimes(),
'memory_usage' => memory_get_usage(true),
'request_time' => microtime(true) - app()->getBeginTime(),
'cache_hits' => app()->cache->getStats()['hits'] ?? 0
];
// 存储到时序数据库
self::storeMetrics($tenant->id, $metrics);
}
}
七、总结与最佳实践
7.1 架构优势总结
- 数据安全性:通过数据库级别的隔离,确保租户数据绝对安全
- 系统可扩展性:支持水平扩展,可轻松应对租户数量增长
- 维护便捷性:单一代码库维护,降低运维成本
- 资源利用率:共享基础设施,提高资源使用效率
7.2 实施建议
- 渐进式实施:从共享数据库+租户ID隔离开始,逐步过渡到独立数据库
- 监控先行:在系统上线前建立完善的监控体系
- 备份策略:为每个租户制定独立的备份和恢复方案
- 文档完善:详细记录租户管理流程和应急预案
7.3 常见问题解决方案
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 数据库连接数过多 | 系统响应变慢,连接超时 | 使用连接池技术,设置合理的连接超时和回收策略 |
| 租户数据迁移困难 | 迁移过程中服务中断 | 采用蓝绿部署,逐步迁移,确保回滚能力 |
| 缓存污染 | 租户间数据泄露 | 缓存键必须包含租户标识,使用独立的缓存数据库 |

