免费资源下载
发布日期:2023年11月 | 作者:ThinkPHP架构专家
第一章:多租户SaaS系统架构概述
在当今云原生时代,多租户SaaS(Software as a Service)系统已成为企业级应用的主流架构。ThinkPHP 8.0凭借其强大的扩展性和灵活性,为构建高性能多租户系统提供了理想的技术栈。
多租户数据隔离的三种模式:
- 独立数据库模式:每个租户拥有独立的数据库实例
- 共享数据库独立Schema:共享数据库,但使用不同的Schema
- 共享数据库共享Schema:通过tenant_id字段进行数据隔离
ThinkPHP多租户架构优势:
// ThinkPHP 8.0 提供了以下核心特性支持多租户:
// 1. 动态数据库配置
// 2. 中间件租户识别
// 3. 模型作用域自动过滤
// 4. 事件驱动的租户上下文管理
// 5. 缓存隔离机制
第二章:ThinkPHP多租户架构设计
2.1 系统架构图
我们采用混合模式架构,结合独立数据库与共享数据库的优势:
核心组件设计:
app/
├── common/
│ ├── middleware/
│ │ └── TenantMiddleware.php # 租户识别中间件
│ ├── service/
│ │ └── TenantService.php # 租户服务
│ └── library/
│ └── DatabaseRouter.php # 数据库路由
├── model/
│ ├── trait/
│ │ └── TenantScope.php # 租户作用域特性
│ └── Tenant.php # 租户模型
└── config/
└── tenant.php # 租户配置
2.2 租户上下文管理
<?php
namespace appcommonlibrary;
use thinkfacadeDb;
use thinkfacadeCache;
class TenantContext
{
// 租户上下文单例
private static $instance;
private $currentTenant;
private $tenantConfig;
private function __construct() {}
public static function getInstance(): self
{
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 设置当前租户
*/
public function setTenant(array $tenantInfo): void
{
$this->currentTenant = $tenantInfo;
$this->tenantConfig = $this->loadTenantConfig($tenantInfo['id']);
// 设置数据库连接
$this->setupDatabaseConnection();
// 设置缓存前缀
$this->setupCachePrefix();
}
/**
* 动态配置数据库连接
*/
private function setupDatabaseConnection(): void
{
$config = [
// 默认连接(共享库)
'default' => config('database.default'),
// 租户独立数据库
'connections' => [
'tenant_db' => [
'type' => 'mysql',
'hostname' => $this->tenantConfig['db_host'],
'database' => $this->tenantConfig['db_name'],
'username' => $this->tenantConfig['db_user'],
'password' => $this->tenantConfig['db_password'],
'charset' => 'utf8mb4',
'prefix' => 't_' . $this->currentTenant['id'] . '_',
]
]
];
Db::setConfig($config);
}
/**
* 获取当前租户ID
*/
public function getTenantId(): string
{
return $this->currentTenant['id'] ?? '';
}
}
第三章:核心实现 – 租户中间件与路由
3.1 租户识别中间件
<?php
namespace appcommonmiddleware;
use thinkRequest;
use thinkResponse;
use appcommonlibraryTenantContext;
use appcommonserviceTenantService;
class TenantMiddleware
{
public function handle(Request $request, Closure $next): Response
{
// 从请求中识别租户
$tenantId = $this->extractTenantId($request);
if (!$tenantId) {
return json(['code' => 401, 'msg' => '租户标识缺失']);
}
// 验证租户有效性
$tenantService = new TenantService();
$tenantInfo = $tenantService->getTenantById($tenantId);
if (!$tenantInfo || $tenantInfo['status'] !== 'active') {
return json(['code' => 403, 'msg' => '租户不存在或已禁用']);
}
// 设置租户上下文
TenantContext::getInstance()->setTenant($tenantInfo);
// 注入租户ID到请求
$request->tenantId = $tenantId;
return $next($request);
}
/**
* 从不同来源提取租户ID
*/
private function extractTenantId(Request $request): ?string
{
// 1. 子域名识别:tenant1.app.com
$host = $request->host();
$subdomain = explode('.', $host)[0];
if ($subdomain && $subdomain !== 'www') {
return $subdomain;
}
// 2. 请求头识别:X-Tenant-ID
if ($request->header('X-Tenant-ID')) {
return $request->header('X-Tenant-ID');
}
// 3. 查询参数识别:?tenant_id=xxx
if ($request->param('tenant_id')) {
return $request->param('tenant_id');
}
// 4. JWT Token识别
$authorization = $request->header('Authorization');
if ($authorization && strpos($authorization, 'Bearer ') === 0) {
$token = substr($authorization, 7);
$payload = $this->decodeJWT($token);
return $payload['tenant_id'] ?? null;
}
return null;
}
}
3.2 模型租户作用域
<?php
namespace appmodeltrait;
use thinkModel;
use appcommonlibraryTenantContext;
trait TenantScope
{
/**
* 初始化租户作用域
*/
protected static function bootTenantScope(): void
{
static::addGlobalScope('tenant', function ($query) {
$tenantId = TenantContext::getInstance()->getTenantId();
if ($tenantId && self::hasTenantColumn()) {
$query->where('tenant_id', $tenantId);
}
});
// 自动设置tenant_id
static::event('before_insert', function ($model) {
$tenantId = TenantContext::getInstance()->getTenantId();
if ($tenantId && self::hasTenantColumn()) {
$model->setAttr('tenant_id', $tenantId);
}
});
}
/**
* 检查模型是否有tenant_id字段
*/
private static function hasTenantColumn(): bool
{
$model = new static();
return in_array('tenant_id', $model->getTableFields());
}
/**
* 移除租户作用域(用于超级管理员)
*/
public function scopeWithoutTenant($query)
{
$query->removeGlobalScope('tenant');
return $query;
}
}
// 在模型中使用
namespace appmodel;
use thinkModel;
use appmodeltraitTenantScope;
class Product extends Model
{
use TenantScope;
// 所有查询自动添加 tenant_id 条件
// 插入时自动设置 tenant_id
}
第四章:多租户数据库策略实战
4.1 混合模式数据库设计
<?php
namespace appcommonservice;
use thinkfacadeDb;
class DatabaseStrategy
{
/**
* 根据租户级别选择数据库策略
*/
public function getConnectionConfig(string $tenantId, string $tenantLevel): array
{
$baseConfig = config('database.connections.mysql');
switch ($tenantLevel) {
case 'premium': // 高级用户 - 独立数据库
return [
'type' => 'mysql',
'hostname' => 'cluster.db.premium.com',
'database' => 'tenant_' . $tenantId,
'username' => 'user_' . $tenantId,
'password' => $this->generatePassword($tenantId),
'charset' => 'utf8mb4',
'prefix' => '',
'break_reconnect' => true,
'break_match_str' => ['MySQL server has gone away'],
];
case 'standard': // 标准用户 - 共享数据库独立schema
return array_merge($baseConfig, [
'database' => 'shared_database',
'prefix' => 't_' . $tenantId . '_',
]);
case 'basic': // 基础用户 - 共享表通过tenant_id隔离
default:
return array_merge($baseConfig, [
'database' => 'shared_database',
'prefix' => '',
]);
}
}
/**
* 动态切换数据库连接
*/
public function switchConnection(string $connectionName, array $config): void
{
Db::connect($connectionName, $config);
}
/**
* 执行跨租户查询(仅管理员)
*/
public function crossTenantQuery(string $sql, array $params = []): array
{
$results = [];
$tenants = Db::name('tenants')->where('status', 'active')->select();
foreach ($tenants as $tenant) {
// 切换到租户数据库
$config = $this->getConnectionConfig($tenant['id'], $tenant['level']);
$this->switchConnection('temp_connection', $config);
// 执行查询
$result = Db::connect('temp_connection')->query($sql, $params);
$results[$tenant['id']] = $result;
}
return $results;
}
}
4.2 数据库迁移与分片
<?php
// 数据库迁移命令 - 为租户创建独立schema
namespace appcommand;
use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;
use thinkfacadeDb;
class TenantMigrate extends Command
{
protected function configure()
{
$this->setName('tenant:migrate')
->setDescription('为租户执行数据库迁移');
}
protected function execute(Input $input, Output $output)
{
// 获取所有活跃租户
$tenants = Db::name('tenants')
->where('status', 'active')
->select();
foreach ($tenants as $tenant) {
$output->writeln("正在为租户 {$tenant['name']} 执行迁移...");
// 切换到租户数据库
$this->switchToTenant($tenant['id']);
// 执行迁移
$this->runMigrations();
// 初始化基础数据
$this->seedInitialData($tenant['id']);
$output->writeln("租户 {$tenant['name']} 迁移完成");
}
}
/**
* 数据库分片策略
*/
private function getShardDatabase(string $tenantId): string
{
// 基于租户ID哈希分片
$shardCount = 16; // 16个分片
$shardIndex = hexdec(substr(md5($tenantId), 0, 8)) % $shardCount;
return 'shard_db_' . str_pad($shardIndex, 2, '0', STR_PAD_LEFT);
}
}
第五章:安全隔离与数据保护
5.1 租户数据隔离验证
<?php
namespace appcommonmiddleware;
use thinkRequest;
use thinkResponse;
class TenantDataIsolation
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// 验证响应数据不包含其他租户数据
if ($response->getData() instanceof thinkCollection) {
$this->validateTenantData($response->getData());
}
return $response;
}
/**
* 验证数据租户隔离
*/
private function validateTenantData($data): void
{
$currentTenantId = request()->tenantId;
if ($data instanceof thinkCollection) {
foreach ($data as $item) {
if (isset($item['tenant_id']) && $item['tenant_id'] !== $currentTenantId) {
throw new Exception('数据隔离验证失败:检测到跨租户数据访问');
}
}
}
}
}
/**
* 租户数据访问日志
*/
class TenantAccessLogger
{
public static function log(string $action, array $data = []): void
{
$logData = [
'tenant_id' => request()->tenantId,
'user_id' => request()->userId ?? null,
'action' => $action,
'url' => request()->url(),
'ip' => request()->ip(),
'user_agent' => request()->header('user-agent'),
'data' => json_encode($data, JSON_UNESCAPED_UNICODE),
'created_at' => date('Y-m-d H:i:s')
];
// 写入租户专属日志表
Db::connect('tenant_log')->name('access_logs')->insert($logData);
}
}
第六章:性能优化与缓存策略
6.1 多级缓存架构
<?php
namespace appcommonlibrarycache;
use thinkfacadeCache;
class TenantCache
{
private $tenantId;
public function __construct(string $tenantId)
{
$this->tenantId = $tenantId;
}
/**
* 获取带租户前缀的缓存键
*/
public function key(string $key): string
{
return "tenant:{$this->tenantId}:{$key}";
}
/**
* 租户级缓存设置
*/
public function set(string $key, $value, int $ttl = 3600): bool
{
return Cache::set($this->key($key), $value, $ttl);
}
/**
* 租户数据缓存策略
*/
public function remember(string $key, callable $callback, int $ttl = 3600)
{
$cacheKey = $this->key($key);
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$value = $callback();
Cache::set($cacheKey, $value, $ttl);
return $value;
}
/**
* 清除租户所有缓存
*/
public function clearAll(): void
{
$pattern = "tenant:{$this->tenantId}:*";
// Redis批量删除
$redis = Cache::store('redis')->handler();
$keys = $redis->keys($pattern);
if (!empty($keys)) {
$redis->del($keys);
}
}
}
/**
* 数据库连接池管理
*/
class ConnectionPool
{
private static $pools = [];
/**
* 获取租户数据库连接池
*/
public static function getConnection(string $tenantId): thinkdbConnection
{
if (!isset(self::$pools[$tenantId])) {
self::$pools[$tenantId] = self::createConnectionPool($tenantId);
}
return self::$pools[$tenantId]->getConnection();
}
/**
* 创建连接池
*/
private static function createConnectionPool(string $tenantId): Pool
{
return new Pool(function () use ($tenantId) {
$config = TenantService::getDbConfig($tenantId);
return Db::connect($config);
}, [
'max_connections' => 20,
'max_idle_time' => 300,
]);
}
}
6.2 查询优化与索引策略
<?php
// 租户数据查询优化器
class TenantQueryOptimizer
{
/**
* 自动添加租户索引提示
*/
public static function optimizeQuery(thinkdbQuery $query): thinkdbQuery
{
$table = $query->getTable();
$tenantId = request()->tenantId;
// 检查表是否有tenant_id索引
if ($this->hasTenantIndex($table)) {
// 强制使用tenant_id索引
$query->forceIndex("idx_tenant_id");
}
// 添加查询缓存提示
if ($this->isCacheableQuery($query)) {
$query->cache("tenant_query:" . md5($query->buildSql()), 300);
}
return $query;
}
/**
* 分页查询优化
*/
public static function paginateOptimized(thinkdbQuery $query, int $page, int $size): array
{
// 使用覆盖索引优化count查询
$total = $query->count('id', true);
// 使用延迟关联优化大数据量分页
if ($total > 10000) {
$ids = $query->page($page, $size)
->order('id', 'desc')
->column('id');
$data = $query->whereIn('id', $ids)
->order('id', 'desc')
->select();
} else {
$data = $query->page($page, $size)
->order('id', 'desc')
->select();
}
return [
'total' => $total,
'data' => $data,
'page' => $page,
'size' => $size,
'pages' => ceil($total / $size)
];
}
}
总结与最佳实践
核心要点总结:
- 架构选择:根据业务需求选择合适的隔离级别,混合模式提供最佳灵活性
- 中间件设计:统一的租户识别中间件确保所有请求正确路由
- 模型作用域:自动化的租户数据过滤避免数据泄露风险
- 缓存策略:租户级别的缓存隔离和清理机制
- 安全审计:完整的访问日志和权限验证体系
部署建议:
# Docker多租户部署示例
version: '3.8'
services:
app:
build: .
environment:
- TENANT_MODE=hybrid
- DB_SHARD_COUNT=16
- CACHE_PREFIX=tenant_
deploy:
replicas: 3
tenant_db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
volumes:
- tenant_data:/var/lib/mysql
command:
- --innodb_buffer_pool_size=2G
- --innodb_log_file_size=512M
redis:
image: redis:6-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
通过本文的完整实现方案,您可以基于ThinkPHP 8.0构建出高性能、安全可靠的多租户SaaS系统。该架构已在多个生产环境中验证,支持千级租户和百万级用户并发访问。

