免费资源下载
发布日期:2023年11月
作者:PHP架构师
阅读时间:15分钟
作者:PHP架构师
阅读时间:15分钟
一、多租户SaaS系统概述
多租户SaaS(Software as a Service)架构允许单个应用实例为多个客户(租户)提供服务,每个租户的数据和配置相互隔离。在ThinkPHP框架下实现多租户系统需要考虑以下关键问题:
1.1 多租户数据隔离模式
- 独立数据库模式:每个租户拥有独立的数据库,安全性最高
- 共享数据库独立模式:共享数据库,通过schema隔离数据
- 共享数据库共享模式:通过tenant_id字段区分数据,成本最低
1.2 ThinkPHP 6.x的技术优势
ThinkPHP 6.x提供了以下特性,特别适合构建多租户系统:
// ThinkPHP 6.x的多数据库支持
'connections' => [
'tenant_master' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => 'saas_master',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'deploy' => 0,
'rw_separate' => true, // 读写分离
]
]
二、ThinkPHP多租户架构设计
2.1 系统架构图
┌─────────────────────────────────────────┐
│ 客户端请求 │
└─────────────────┬───────────────────────┘
│
┌───────▼────────┐
│ 负载均衡/Nginx │
└───────┬────────┘
│
┌───────▼────────┐
│ 租户识别中间件 │
│ (TenantResolver)│
└───────┬────────┘
│
┌───────▼────────┐
│ 数据库路由中间件 │
│ (DbRouter) │
└───────┬────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│租户A库 │ │租户B库 │ │主控库 │
│ │ │ │ │ │
└───────┘ └───────┘ └───────┘
2.2 目录结构设计
app/
├── common/ # 公共模块
│ ├── library/ # 公共库
│ ├── model/ # 公共模型
│ └── service/ # 公共服务
├── tenant/ # 租户业务模块
│ ├── controller/
│ ├── model/
│ └── service/
├── admin/ # 管理后台
└── middleware/ # 中间件
├── TenantAuth.php # 租户认证
├── TenantDb.php # 数据库路由
└── TenantLog.php # 租户日志
三、核心模块实现
3.1 租户识别中间件
<?php
namespace appmiddleware;
use thinkfacadeDb;
use thinkfacadeConfig;
class TenantResolver
{
public function handle($request, Closure $next)
{
// 从子域名识别租户
$host = $request->host();
$subdomain = $this->extractSubdomain($host);
// 从请求头识别(API场景)
$tenantId = $request->header('X-Tenant-Id');
// 从JWT Token识别
if (!$tenantId && $request->bearerToken()) {
$tenantId = $this->parseTenantFromToken($request->bearerToken());
}
// 查询租户信息
$tenant = Db::connect('master')
->name('tenants')
->where('identifier', $subdomain)
->where('status', 1)
->find();
if (!$tenant) {
throw new thinkexceptionHttpException(404, '租户不存在或已禁用');
}
// 存储租户信息到请求上下文
$request->tenant = $tenant;
// 设置租户上下文
app()->tenant = $tenant;
return $next($request);
}
private function extractSubdomain($host)
{
$parts = explode('.', $host);
if (count($parts) > 2) {
return $parts[0];
}
return 'www'; // 默认租户
}
private function parseTenantFromToken($token)
{
// JWT解析逻辑
// 实际项目中应使用完整的JWT验证
return null;
}
}
3.2 动态数据库连接管理
<?php
namespace appcommonlibrary;
class TenantDbManager
{
protected static $connections = [];
/**
* 获取租户数据库连接
*/
public static function getConnection($tenantId)
{
if (isset(self::$connections[$tenantId])) {
return self::$connections[$tenantId];
}
// 从配置中心或数据库获取租户数据库配置
$config = self::getTenantDbConfig($tenantId);
// 动态创建数据库连接
$connectionName = "tenant_{$tenantId}";
Config::set([
"database.connections.{$connectionName}" => $config
]);
self::$connections[$tenantId] = $connectionName;
return $connectionName;
}
/**
* 获取租户数据库配置
*/
protected static function getTenantDbConfig($tenantId)
{
// 这里可以从主数据库查询租户的数据库配置
// 或者从配置中心获取
$masterDb = Db::connect('master');
$tenantConfig = $masterDb->name('tenant_databases')
->where('tenant_id', $tenantId)
->find();
return [
'type' => 'mysql',
'hostname' => $tenantConfig['host'],
'database' => $tenantConfig['database'],
'username' => $tenantConfig['username'],
'password' => self::decryptPassword($tenantConfig['password']),
'hostport' => $tenantConfig['port'] ?? 3306,
'charset' => 'utf8mb4',
'prefix' => 't_' . $tenantId . '_', // 租户前缀
'break_reconnect' => true,
'trigger_sql' => true,
'fields_cache' => false,
];
}
/**
* 切换数据库连接
*/
public static function switchToTenant($tenantId)
{
$connection = self::getConnection($tenantId);
Db::connect($connection);
// 设置模型默认连接
thinkModel::setConnection($connection);
}
}
3.3 多租户模型基类
<?php
namespace appcommonmodel;
use thinkModel;
abstract class TenantModel extends Model
{
// 自动写入租户ID
protected $tenantField = 'tenant_id';
// 自动时间戳
protected $autoWriteTimestamp = true;
// 全局查询范围
protected function base($query)
{
$tenantId = app()->tenant->id ?? null;
if ($tenantId) {
$query->where($this->tenantField, $tenantId);
}
}
/**
* 初始化方法
*/
protected static function init()
{
// 插入前自动设置租户ID
self::event('before_insert', function ($model) {
$tenantId = app()->tenant->id ?? null;
if ($tenantId && isset($model->tenantField)) {
$model->{$model->tenantField} = $tenantId;
}
});
// 更新前验证租户权限
self::event('before_update', function ($model) {
self::checkTenantPermission($model);
});
// 删除前验证租户权限
self::event('before_delete', function ($model) {
self::checkTenantPermission($model);
});
}
/**
* 检查租户权限
*/
protected static function checkTenantPermission($model)
{
$tenantId = app()->tenant->id ?? null;
if ($tenantId && isset($model->tenantField)) {
if ($model->{$model->tenantField} != $tenantId) {
throw new thinkexceptionValidateException('无权操作其他租户数据');
}
}
}
/**
* 获取当前租户的查询实例
*/
public static function tenantQuery()
{
$model = new static();
$tenantId = app()->tenant->id ?? null;
if ($tenantId && $model->tenantField) {
return static::where($model->tenantField, $tenantId);
}
return new static();
}
}
3.4 租户数据迁移系统
<?php
namespace appcommand;
use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;
use thinkfacadeDb;
class TenantMigrate extends Command
{
protected function configure()
{
$this->setName('tenant:migrate')
->setDescription('执行租户数据库迁移')
->addArgument('tenant_id', null, '租户ID(为空则迁移所有租户)');
}
protected function execute(Input $input, Output $output)
{
$tenantId = $input->getArgument('tenant_id');
if ($tenantId) {
$this->migrateTenant($tenantId, $output);
} else {
$this->migrateAllTenants($output);
}
}
/**
* 迁移单个租户
*/
protected function migrateTenant($tenantId, $output)
{
$output->writeln("开始迁移租户 {$tenantId}");
// 切换到租户数据库
$connection = appcommonlibraryTenantDbManager::getConnection($tenantId);
Db::connect($connection);
// 执行迁移文件
$migrationPath = app()->getRootPath() . 'database/migrations/';
$files = glob($migrationPath . '*.php');
foreach ($files as $file) {
$output->writeln("执行迁移: " . basename($file));
include_once $file;
$className = '\database\migrations\' . basename($file, '.php');
if (class_exists($className)) {
$migration = new $className();
$migration->up();
// 记录迁移历史
$this->recordMigration($tenantId, basename($file));
}
}
$output->writeln("租户 {$tenantId} 迁移完成");
}
/**
* 迁移所有租户
*/
protected function migrateAllTenants($output)
{
$tenants = Db::connect('master')
->name('tenants')
->where('status', 1)
->select();
foreach ($tenants as $tenant) {
$this->migrateTenant($tenant['id'], $output);
}
}
/**
* 记录迁移历史
*/
protected function recordMigration($tenantId, $migration)
{
$data = [
'tenant_id' => $tenantId,
'migration' => $migration,
'batch' => time(),
'created_at' => date('Y-m-d H:i:s')
];
Db::connect('master')
->name('tenant_migrations')
->insert($data);
}
}
四、安全与数据隔离
4.1 数据隔离策略
<?php
namespace appcommonservice;
class DataIsolation
{
/**
* 行级数据隔离
*/
public static function applyRowLevelIsolation($query, $tenantId)
{
// 自动添加租户条件
$query->where('tenant_id', $tenantId);
// 对于关联查询,自动关联租户条件
$query->whereExists(function ($subQuery) use ($tenantId) {
$subQuery->table('related_table')
->where('tenant_id', $tenantId)
->whereRaw('related_table.id = main_table.related_id');
});
}
/**
* 数据库级隔离
*/
public static function switchDatabase($tenantId)
{
$config = self::getTenantDatabaseConfig($tenantId);
// 使用不同的数据库用户,限制权限
$config['username'] = 'tenant_' . $tenantId;
$config['database'] = 'saas_tenant_' . $tenantId;
return $config;
}
/**
* 数据加密存储
*/
public static function encryptSensitiveData($data, $tenantId)
{
// 使用租户特定的加密密钥
$key = self::getTenantEncryptionKey($tenantId);
// 使用OpenSSL加密
$iv = random_bytes(16);
$encrypted = openssl_encrypt(
$data,
'AES-256-CBC',
$key,
OPENSSL_RAW_DATA,
$iv
);
return base64_encode($iv . $encrypted);
}
}
4.2 租户间API访问控制
<?php
namespace appcommonmiddleware;
class TenantApiAuth
{
public function handle($request, Closure $next)
{
// 验证API密钥
$apiKey = $request->header('X-Api-Key');
$tenantId = $request->header('X-Tenant-Id');
if (!$this->validateApiKey($apiKey, $tenantId)) {
throw new thinkexceptionHttpException(401, '无效的API密钥');
}
// 检查API访问频率限制
if (!$this->checkRateLimit($tenantId)) {
throw new thinkexceptionHttpException(429, '访问频率超限');
}
// 记录API访问日志
$this->logApiAccess($tenantId, $request);
return $next($request);
}
private function validateApiKey($apiKey, $tenantId)
{
$cacheKey = "api_key:{$tenantId}:{$apiKey}";
// 从缓存检查
if (cache($cacheKey)) {
return true;
}
// 从数据库验证
$valid = Db::connect('master')
->name('tenant_api_keys')
->where('tenant_id', $tenantId)
->where('api_key', hash('sha256', $apiKey))
->where('status', 1)
->where('expires_at', '>', date('Y-m-d H:i:s'))
->find();
if ($valid) {
// 缓存验证结果
cache($cacheKey, true, 300);
return true;
}
return false;
}
}
五、性能优化策略
5.1 多级缓存架构
<?php
namespace appcommonlibrarycache;
class TenantCache
{
protected $tenantId;
protected $prefix;
public function __construct($tenantId)
{
$this->tenantId = $tenantId;
$this->prefix = "tenant:{$tenantId}:";
}
/**
* 获取租户缓存(带自动降级)
*/
public function get($key, $default = null, $fallback = null)
{
// 第一级:内存缓存(如Swoole Table)
$value = $this->getFromMemory($key);
if ($value !== null) {
return $value;
}
// 第二级:Redis缓存
$value = cache($this->prefix . $key);
if ($value !== null) {
$this->setToMemory($key, $value);
return $value;
}
// 第三级:数据库查询(缓存穿透保护)
if ($fallback && is_callable($fallback)) {
// 使用互斥锁防止缓存击穿
$lockKey = $this->prefix . $key . ':lock';
if ($this->acquireLock($lockKey)) {
try {
$value = call_user_func($fallback);
$this->set($key, $value, 3600);
return $value;
} finally {
$this->releaseLock($lockKey);
}
}
// 等待其他进程生成缓存
usleep(100000); // 100ms
return $this->get($key, $default, null);
}
return $default;
}
/**
* 批量获取缓存
*/
public function getMultiple(array $keys)
{
$results = [];
$missingKeys = [];
foreach ($keys as $key) {
$value = $this->getFromMemory($key);
if ($value !== null) {
$results[$key] = $value;
} else {
$missingKeys[] = $key;
}
}
if (!empty($missingKeys)) {
$redisKeys = array_map(function ($key) {
return $this->prefix . $key;
}, $missingKeys);
$redisValues = cache()->getMultiple($redisKeys);
foreach ($missingKeys as $index => $key) {
$value = $redisValues[$index] ?? null;
$results[$key] = $value;
if ($value !== null) {
$this->setToMemory($key, $value);
}
}
}
return $results;
}
}
5.2 数据库连接池优化
<?php
namespace appcommonpool;
use SwooleDatabasePDOPool;
use thinkfacadeConfig;
class DatabasePoolManager
{
protected static $pools = [];
/**
* 获取数据库连接池
*/
public static function getPool($tenantId)
{
if (!isset(self::$pools[$tenantId])) {
$config = self::getTenantDbConfig($tenantId);
self::$pools[$tenantId] = new PDOPool(
(new SwooleDatabasePDOConfig())
->withHost($config['hostname'])
->withPort($config['hostport'] ?? 3306)
->withDbName($config['database'])
->withCharset($config['charset'])
->withUsername($config['username'])
->withPassword($config['password']),
Config::get('database.pool.max_connections', 20)
);
}
return self::$pools[$tenantId];
}
/**
* 使用连接池执行查询
*/
public static function execute($tenantId, callable $callback)
{
$pool = self::getPool($tenantId);
$pdo = $pool->get();
try {
return $callback($pdo);
} finally {
$pool->put($pdo);
}
}
}
六、部署与运维
6.1 Docker容器化部署
# docker-compose.yml
version: '3.8'
services:
# 主数据库
db-master:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: saas_master
volumes:
- db-master-data:/var/lib/mysql
- ./docker/mysql/master.cnf:/etc/mysql/conf.d/custom.cnf
networks:
- saas-network
# 租户数据库集群
db-tenant-1:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db-tenant-1-data:/var/lib/mysql
- ./docker/mysql/tenant.cnf:/etc/mysql/conf.d/custom.cnf
networks:
- saas-network
# Redis集群
redis:
image: redis:6-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- saas-network
# PHP应用
app:
build:
context: .
dockerfile: Dockerfile
depends_on:
- db-master
- redis
environment:
APP_DEBUG: ${APP_DEBUG}
DB_HOST: db-master
REDIS_HOST: redis
volumes:
- ./:/var/www/html
networks:
- saas-network
# Nginx
nginx:
image: nginx:1.21-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/conf.d:/etc/nginx/conf.d
- ./public:/var/www/html/public
depends_on:
- app
networks:
- saas-network
networks:
saas-network:
driver: bridge
volumes:
db-master-data:
db-tenant-1-data:
redis-data:
6.2 自动化监控脚本
<?php
// app/command/TenantMonitor.php
namespace appcommand;
use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;
use thinkfacadeDb;
class TenantMonitor extends Command
{
protected function configure()
{
$this->setName('tenant:monitor')
->setDescription('监控租户系统状态');
}
protected function execute(Input $input, Output $output)
{
$this->checkDatabaseConnections($output);
$this->checkTenantStatus($output);
$this->checkResourceUsage($output);
$this->sendAlertsIfNeeded($output);
}
protected function checkDatabaseConnections(Output $output)
{
$output->writeln('检查数据库连接...');
$tenants = Db::connect('master')
->name('tenants')
->where('status', 1)
->select();
foreach ($tenants as $tenant) {
try {
$connection = appcommonlibraryTenantDbManager::getConnection($tenant['id']);
Db::connect($connection)->query('SELECT 1');
$output->writeln("租户 {$tenant['name']} 数据库连接正常");
} catch (Exception $e) {
$output->writeln("租户 {$tenant['name']} 数据库连接失败: {$e->getMessage()}");
$this->sendAlert("租户 {$tenant['name']} 数据库连接异常");
}
}
}
protected function checkResourceUsage(Output $output)
{
$output->writeln('检查资源使用情况...');
// 检查磁盘空间
$freeSpace = disk_free_space('/');
$totalSpace = disk_total_space('/');
$usagePercent = (1 - $freeSpace / $totalSpace) * 100;
if ($usagePercent > 90) {
$output->writeln("磁盘空间不足: {$usagePercent}% 已使用");
$this->sendAlert("磁盘空间不足: {$usagePercent}% 已使用");
}
// 检查内存使用
$memoryUsage = memory_get_usage(true) / 1024 / 1024; // MB
if ($memoryUsage > 512) { // 超过512MB
$output->writeln("内存使用较高: {$memoryUsage}MB");
}
}
}
6.3 备份与恢复策略
<?php
namespace appcommonservicebackup;
class TenantBackupService
{
/**
* 备份租户数据
*/
public function backupTenant($tenantId, $type = 'full')
{
$backupId = uniqid('backup_');
$backupPath = $this->getBackupPath($tenantId, $backupId);
// 创建备份目录
if (!is_dir($backupPath)) {
mkdir($backupPath, 0755, true);
}
// 备份数据库
$this->backupDatabase($tenantId, $backupPath);
// 备份文件(如果配置了文件存储)
if ($type === 'full') {
$this->backupFiles($tenantId, $backupPath);
}
// 生成备份元数据
$this->createBackupMetadata($tenantId, $backupId, $type, $backupPath);
// 上传到云存储
$this->uploadToCloudStorage($backupPath);
// 清理旧备份
$this->cleanOldBackups($tenantId);
return $backupId;
}
/**
* 备份数据库
*/
protected function backupDatabase($tenantId, $backupPath)
{
$config = appcommonlibraryTenantDbManager::getTenantDbConfig($tenantId);
$filename = $backupPath . '/database.sql';
$command = sprintf(
'mysqldump -h%s -u%s -p%s %s > %s',
escapeshellarg($config['hostname']),
escapeshellarg($config['username']),
escapeshellarg($config['password']),
escapeshellarg($config['database']),
escapeshellarg($filename)
);
exec($command, $output, $returnVar);
if ($returnVar !== 0) {
throw new Exception('数据库备份失败');
}
// 压缩备份文件
$this->compressFile($filename);
}
/**
* 恢复租户数据
*/
public function restoreTenant($tenantId, $backupId)
{
// 进入维护模式
$this->enableMaintenanceMode($tenantId);
try {
// 下载备份文件
$backupPath = $this->downloadBackup($tenantId, $backupId);
// 恢复数据库
$this->restoreDatabase($tenantId, $backupPath);
// 恢复文件
$this->restoreFiles($tenantId, $backupPath);
// 更新恢复记录
$this->recordRestoration($tenantId, $backupId);
} finally {
// 退出维护模式
$this->disableMaintenanceMode($tenantId);
}
}
}
七、总结与最佳实践
7.1 关键成功因素
- 架构设计先行:在项目开始前确定合适的多租户模式
- 自动化运维:实现租户的自动化创建、备份和监控
- 性能监控:建立完善的性能监控和告警系统
- 安全加固:实施多层次的安全防护措施
- 文档完善:保持技术文档和操作手册的更新
7.2 常见问题解决方案
| 问题 | 解决方案 |
|---|---|
| 租户数据交叉访问 | 使用全局查询范围+中间件双重验证 |
| 数据库连接数过多 | 实现连接池+连接复用机制 |
| 迁移维护困难 | 自动化迁移脚本+版本控制 |
| 性能随租户增加下降 | 水平分库+读写分离+缓存优化 |
7.3 未来扩展方向
- 微服务化改造:将单体应用拆分为微服务架构
- 多区域部署:支持跨地域的数据中心部署
- AI集成:集成机器学习能力提供智能服务
- Serverless支持:支持无服务器架构部署
- 开放平台:提供API开放平台支持第三方集成

