一、多租户架构概述
多租户架构是现代SaaS应用的核心设计模式,允许单个应用实例为多个租户提供服务,同时保证数据的安全隔离。本文将深入探讨基于ThinkPHP 6.0的多租户系统完整实现方案。
二、架构设计模式选择
2.1 数据库隔离方案对比
- 独立数据库:每个租户使用独立数据库,安全性最高
- 共享数据库独立Schema:同一数据库,不同数据表结构
- 共享数据库共享Schema:通过tenant_id字段进行数据隔离
2.2 技术选型理由
选择共享数据库共享Schema方案,平衡了开发复杂度与系统性能,适合中小型SaaS应用快速迭代。
三、核心代码实现
3.1 中间件实现租户识别
namespace appmiddleware;
class TenantMiddleware
{
public function handle($request, Closure $next)
{
// 从子域名识别租户
$subdomain = explode('.', $request->host())[0];
// 从请求头识别租户
$tenantId = $request->header('X-Tenant-ID');
// 查询租户信息
$tenant = appmodelTenant::where('identifier', $subdomain)
->whereOr('id', $tenantId)
->find();
if (!$tenant) {
throw new thinkexceptionHttpException(404, '租户不存在');
}
// 设置全局租户上下文
appfacadeTenantContext::setTenant($tenant);
return $next($request);
}
}
3.2 租户上下文管理
namespace appfacade;
use thinkFacade;
class TenantContext extends Facade
{
protected static function getFacadeClass()
{
return appserviceTenantService::class;
}
}
namespace appservice;
class TenantService
{
protected $tenant;
public function setTenant($tenant)
{
$this->tenant = $tenant;
return $this;
}
public function getTenant()
{
return $this->tenant;
}
public function getId()
{
return $this->tenant ? $this->tenant->id : null;
}
}
3.3 模型层自动过滤
namespace appmodel;
use thinkModel;
class BaseModel extends Model
{
protected function scopeTenant($query)
{
$tenantId = appfacadeTenantContext::getId();
if ($tenantId) {
$query->where('tenant_id', $tenantId);
}
}
protected static function onBeforeInsert($model)
{
$tenantId = appfacadeTenantContext::getId();
if ($tenantId && !$model->tenant_id) {
$model->tenant_id = $tenantId;
}
}
}
// 业务模型继承
class User extends BaseModel
{
protected $table = 'users';
// 所有查询自动加入租户过滤
public function search($keyword)
{
return $this->tenant()->where('name|email', 'like', "%{$keyword}%")->select();
}
}
四、数据库设计
4.1 租户表结构
CREATE TABLE `tenants` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '租户名称',
`identifier` varchar(50) NOT NULL COMMENT '唯一标识符',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:0-禁用,1-启用',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4.2 业务表结构示例
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tenant_id` int(11) NOT NULL COMMENT '租户ID',
`name` varchar(50) NOT NULL COMMENT '用户姓名',
`email` varchar(100) NOT NULL COMMENT '邮箱',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
KEY `idx_email` (`email`),
CONSTRAINT `fk_user_tenant` FOREIGN KEY (`tenant_id`) REFERENCES `tenants` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
五、路由配置与子域名支持
5.1 路由配置文件
// route/app.php
Route::domain(':subdomain', function () {
Route::get('/', 'index/index');
Route::get('users', 'user/index');
Route::post('users', 'user/create');
Route::put('users/:id', 'user/update');
Route::delete('users/:id', 'user/delete');
})->completeMatch(false);
5.2 控制器实现
namespace appcontroller;
class User extends BaseController
{
public function index()
{
$keyword = $this->request->param('keyword');
$users = appmodelUser::search($keyword)->paginate(10);
return json([
'code' => 200,
'data' => $users->items(),
'total' => $users->total()
]);
}
public function create()
{
$data = $this->request->post();
$user = new appmodelUser();
if ($user->save($data)) {
return json(['code' => 200, 'message' => '创建成功']);
}
return json(['code' => 500, 'message' => '创建失败']);
}
}
六、高级特性实现
6.1 租户自定义配置
namespace appservice;
class TenantConfigService
{
public function getConfig($key, $default = null)
{
$tenantId = TenantContext::getId();
$config = appmodelTenantConfig::where([
'tenant_id' => $tenantId,
'config_key' => $key
])->value('config_value');
return $config ?: $default;
}
public function setConfig($key, $value)
{
$tenantId = TenantContext::getId();
return appmodelTenantConfig::updateOrInsert(
['tenant_id' => $tenantId, 'config_key' => $key],
['config_value' => $value, 'updated_at' => date('Y-m-d H:i:s')]
);
}
}
6.2 数据迁移与种子数据
// 数据库迁移类
class CreateTenantTables
{
public function up()
{
$sql = "
CREATE TABLE IF NOT EXISTS `tenant_{TENANT_ID}_custom_data` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`field1` varchar(255) DEFAULT NULL,
`field2` text,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
";
// 为新租户创建自定义表
$tenants = appmodelTenant::where('status', 1)->select();
foreach ($tenants as $tenant) {
$customSql = str_replace('{TENANT_ID}', $tenant->id, $sql);
thinkfacadeDb::execute($customSql);
}
}
}
七、性能优化策略
7.1 数据库连接池配置
// database.php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => 'saas_main',
'username' => 'root',
'password' => '',
'hostport' => '3306',
'params' => [
PDO::ATTR_PERSISTENT => true, // 持久连接
PDO::ATTR_TIMEOUT => 3
],
'charset' => 'utf8mb4',
'deploy' => 0,
'rw_separate' => true, // 读写分离
'master_num' => 1,
'slave_no' => '',
'fields_strict' => true,
]
]
];
7.2 缓存策略设计
namespace appservice;
class TenantCacheService
{
protected $prefix = 'tenant:';
public function remember($key, $callback, $ttl = 3600)
{
$fullKey = $this->prefix . TenantContext::getId() . ':' . $key;
$data = thinkfacadeCache::get($fullKey);
if (!$data) {
$data = $callback();
thinkfacadeCache::set($fullKey, $data, $ttl);
}
return $data;
}
public function clear($pattern = null)
{
$key = $this->prefix . TenantContext::getId();
if ($pattern) {
$key .= ':' . $pattern;
}
thinkfacadeCache::delete($key . '*');
}
}
八、安全防护措施
8.1 数据越权访问防护
namespace appmiddleware;
class DataPermissionMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
// 验证返回数据是否属于当前租户
if ($response->getData() instanceof thinkCollection) {
foreach ($response->getData() as $item) {
if (isset($item->tenant_id) && $item->tenant_id != appfacadeTenantContext::getId()) {
throw new thinkexceptionHttpException(403, '无权访问该数据');
}
}
}
return $response;
}
}
九、部署与监控
9.1 系统监控指标
- 租户数量增长趋势
- API请求成功率
- 数据库连接池使用率
- 各租户资源消耗统计
9.2 日志记录策略
// 在全局中间件中记录租户操作日志
class OperationLogMiddleware
{
public function handle($request, Closure $next)
{
$startTime = microtime(true);
$response = $next($request);
// 记录操作日志
appmodelOperationLog::create([
'tenant_id' => appfacadeTenantContext::getId(),
'user_id' => $request->user->id ?? 0,
'method' => $request->method(),
'path' => $request->pathinfo(),
'ip' => $request->ip(),
'user_agent' => $request->header('user-agent'),
'response_time' => round((microtime(true) - $startTime) * 1000, 2),
'created_at' => date('Y-m-d H:i:s')
]);
return $response;
}
}
十、总结
本文详细介绍了基于ThinkPHP 6.0的多租户SaaS系统完整架构设计与实现。通过中间件、模型基类、服务层等多层次设计,实现了高效的数据隔离和租户管理。这种架构方案具有良好的扩展性和维护性,能够支撑企业级SaaS应用的快速发展需求。
在实际项目中,还需要考虑租户数据备份恢复、系统升级策略、性能监控告警等更多运维层面的问题。希望本文能为您的ThinkPHP多租户系统开发提供有价值的参考。