发布日期:2024年1月16日 | 作者:PHP架构师
一、多租户SaaS系统概念解析
多租户SaaS(Software as a Service)架构是现代云应用的核心设计模式,它允许单个应用实例为多个客户(租户)提供服务,同时确保数据隔离和安全性。ThinkPHP 6.0凭借其强大的中间件、事件系统和数据库抽象层,成为构建此类系统的理想选择。
多租户实现方案对比:
| 方案类型 | 数据库分离度 | 维护成本 | 适用场景 |
|---|---|---|---|
| 独立数据库 | 完全隔离 | 较高 | 大型企业级应用 |
| 共享数据库独立Schema | 逻辑隔离 | 中等 | 中型SaaS应用 |
| 共享数据库共享Schema | 数据行级隔离 | 较低 | 初创SaaS项目 |
二、系统架构设计
1. 目录结构设计
app/
├── controller/
│ ├── BaseController.php # 基础控制器
│ ├── TenantController.php # 租户相关控制器
│ └── ...
├── middleware/
│ ├── TenantAuth.php # 租户认证中间件
│ └── TenantInitialize.php # 租户初始化中间件
├── model/
│ ├── Tenant.php # 租户模型
│ ├── trait/
│ │ └── TenantScope.php # 租户作用域 trait
│ └── ...
├── service/
│ ├── TenantService.php # 租户服务类
│ └── ...
└── ...
2. 数据库设计
-- 系统租户表
CREATE TABLE `tenants` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '租户名称',
`subdomain` varchar(50) NOT NULL COMMENT '子域名',
`database_name` varchar(100) DEFAULT NULL COMMENT '独立数据库名',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-正常 0-禁用',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_subdomain` (`subdomain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 租户独立数据库中的用户表(示例)
CREATE TABLE `tenant_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tenant_id` int(11) NOT NULL COMMENT '租户ID',
`name` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
三、核心功能实现
1. 租户识别中间件
<?php
namespace appmiddleware;
class TenantInitialize
{
public function handle($request, Closure $next)
{
// 通过子域名识别租户
$subdomain = $this->getSubdomain($request->host());
if (!$subdomain) {
// 默认租户或跳转到主域名
return redirect('https://main-domain.com');
}
// 查询租户信息
$tenant = appmodelTenant::where('subdomain', $subdomain)
->where('status', 1)
->find();
if (!$tenant) {
throw new thinkexceptionHttpException(404, '租户不存在');
}
// 设置当前租户上下文
appserviceTenantService::setCurrentTenant($tenant);
// 如果是独立数据库模式,切换数据库连接
if ($tenant->database_name) {
$this->switchDatabase($tenant->database_name);
}
return $next($request);
}
private function getSubdomain($host)
{
$mainDomain = 'main-domain.com';
if (strpos($host, $mainDomain) !== false) {
return str_replace('.' . $mainDomain, '', $host);
}
return null;
}
private function switchDatabase($databaseName)
{
thinkfacadeDb::connect('tenant_' . $databaseName, [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => $databaseName,
'username' => 'username',
'password' => 'password',
'charset' => 'utf8mb4',
]);
}
}
2. 租户作用域 Trait
<?php
namespace appmodeltrait;
trait TenantScope
{
protected static function bootTenantScope()
{
static::addGlobalScope('tenant', function ($builder) {
$tenantId = appserviceTenantService::getCurrentTenantId();
if ($tenantId) {
$builder->where('tenant_id', $tenantId);
}
});
// 自动设置租户ID
static::creating(function ($model) {
$tenantId = appserviceTenantService::getCurrentTenantId();
if ($tenantId && !isset($model->tenant_id)) {
$model->tenant_id = $tenantId;
}
});
}
// 移除租户作用域
public function scopeWithoutTenant($query)
{
return $query->withoutGlobalScope('tenant');
}
}
3. 租户服务类
<?php
namespace appservice;
class TenantService
{
protected static $currentTenant = null;
public static function setCurrentTenant($tenant)
{
self::$currentTenant = $tenant;
}
public static function getCurrentTenant()
{
return self::$currentTenant;
}
public static function getCurrentTenantId()
{
return self::$currentTenant ? self::$currentTenant->id : null;
}
// 创建新租户
public static function createTenant($data)
{
$tenant = new appmodelTenant();
$tenant->save($data);
// 创建独立数据库
if (config('tenant.database_strategy') === 'isolated') {
self::createTenantDatabase($tenant);
}
return $tenant;
}
private static function createTenantDatabase($tenant)
{
$databaseName = 'tenant_' . $tenant->id . '_' . time();
// 执行数据库创建和表结构初始化
$sql = "CREATE DATABASE IF NOT EXISTS `{$databaseName}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
thinkfacadeDb::execute($sql);
// 初始化表结构
self::initializeTenantDatabase($databaseName);
// 更新租户数据库名称
$tenant->save(['database_name' => $databaseName]);
}
}
4. 租户控制器示例
<?php
namespace appcontroller;
use appBaseController;
use appmodelUser;
class UserController extends BaseController
{
use appmodeltraitTenantScope;
public function index()
{
// 自动应用租户作用域
$users = User::withSearch(['name', 'email'], [
'name' => $this->request->param('name'),
'email' => $this->request->param('email')
])->paginate();
return json([
'code' => 200,
'data' => $users
]);
}
public function create()
{
$data = $this->request->only(['name', 'email', 'password']);
$user = new User();
$user->save($data);
return json([
'code' => 200,
'message' => '用户创建成功',
'data' => $user
]);
}
}
四、高级特性与优化
1. 多数据库连接管理
// database.php 配置
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => 'main_system',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
'tenant_template' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
],
];
2. 租户数据备份与恢复
<?php
namespace appcommand;
use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;
class TenantBackup extends Command
{
protected function configure()
{
$this->setName('tenant:backup')
->setDescription('备份租户数据');
}
protected function execute(Input $input, Output $output)
{
$tenants = appmodelTenant::select();
foreach ($tenants as $tenant) {
if ($tenant->database_name) {
$this->backupTenantDatabase($tenant, $output);
}
}
$output->writeln('所有租户数据备份完成');
}
private function backupTenantDatabase($tenant, $output)
{
$backupFile = runtime_path('backup') . "tenant_{$tenant->id}_" . date('YmdHis') . '.sql';
$command = sprintf(
'mysqldump -u%s -p%s %s > %s',
config('database.connections.tenant_template.username'),
config('database.connections.tenant_template.password'),
$tenant->database_name,
$backupFile
);
system($command, $result);
if ($result === 0) {
$output->writeln("租户 {$tenant->name} 备份成功: {$backupFile}");
} else {
$output->writeln("租户 {$tenant->name} 备份失败");
}
}
}
五、部署与运维
1. 环境配置
// .env 环境配置
APP_DEBUG = false
APP_TRACE = false
[ DATABASE ]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = main_system
USERNAME = root
PASSWORD = your_password
HOSTPORT = 3306
CHARSET = utf8mb4
[ TENANT ]
DATABASE_STRATEGY = isolated # isolated: 独立数据库, shared: 共享数据库
MAX_TENANTS_PER_SERVER = 1000
BACKUP_ENABLED = true
2. Nginx 多租户配置
server {
listen 80;
server_name ~^(?<subdomain>.+).main-domain.com$;
root /path/to/your/project/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# 传递子域名信息
fastcgi_param HTTP_X_TENANT_SUBDOMAIN $subdomain;
}
}
3. 监控与日志
<?php
// 租户操作日志中间件
class TenantLog
{
public function handle($request, Closure $next)
{
$response = $next($request);
// 记录租户操作日志
$tenantId = appserviceTenantService::getCurrentTenantId();
if ($tenantId) {
thinkfacadeLog::write([
'tenant_id' => $tenantId,
'url' => $request->url(),
'method' => $request->method(),
'ip' => $request->ip(),
'user_agent'=> $request->header('user-agent'),
'time' => date('Y-m-d H:i:s')
], 'tenant_operation');
}
return $response;
}
}

