原创作者:陈工程师 | 最后更新:2023年11月
一、多租户SaaS架构核心概念
多租户架构是SaaS(软件即服务)系统的核心设计模式,允许单个应用实例为多个客户(租户)提供服务,同时保持数据隔离和个性化配置。本教程将基于ThinkPHP 6.0实现三种主流的多租户方案。
1.1 三种多租户数据隔离方案
- 独立数据库:每个租户拥有独立的数据库实例
- 共享数据库独立Schema:同一数据库,不同数据表前缀
- 共享数据表:通过tenant_id字段进行数据隔离
二、数据库架构设计
我们采用混合方案:核心租户信息独立存储,业务数据通过tenant_id隔离。
2.1 租户管理表结构
CREATE TABLE `tenants` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '租户名称',
`subdomain` varchar(50) UNIQUE NOT NULL COMMENT '子域名标识',
`database_name` varchar(50) DEFAULT NULL COMMENT '独立数据库名',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-正常 0-禁用',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`tenant_id` int(11) NOT NULL COMMENT '租户ID',
`username` varchar(50) NOT NULL,
`email` varchar(100) NOT NULL,
`password` varchar(255) NOT NULL,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
三、租户识别中间件开发
通过自定义中间件实现基于子域名的租户自动识别。
3.1 创建租户中间件
<?php
namespace appmiddleware;
class TenantMiddleware
{
public function handle($request, Closure $next)
{
// 从子域名获取租户标识
$host = $request->host();
$subdomain = $this->extractSubdomain($host);
if (empty($subdomain)) {
return json(['error' => '租户标识缺失'], 400);
}
// 查询租户信息
$tenant = appmodelTenant::where('subdomain', $subdomain)
->where('status', 1)
->find();
if (!$tenant) {
return json(['error' => '租户不存在或已被禁用'], 404);
}
// 将租户信息绑定到请求对象
$request->tenant = $tenant;
$request->tenantId = $tenant->id;
return $next($request);
}
private function extractSubdomain($host)
{
$parts = explode('.', $host);
if (count($parts) > 2) {
return $parts[0];
}
return null;
}
}
3.2 中间件注册配置
// app/middleware.php
return [
// 全局中间件
appmiddlewareTenantMiddleware::class,
];
四、多租户模型基类实现
创建基础模型类自动处理租户数据隔离。
4.1 多租户模型基类
<?php
namespace appmodel;
use thinkModel;
class BaseModel extends Model
{
protected $tenantId = null;
protected static function boot()
{
parent::boot();
// 全局范围:自动过滤当前租户数据
static::addGlobalScope('tenant', function ($query) {
$tenantId = app('request')->tenantId ?? null;
if ($tenantId && $query->getTableFields()->contains('tenant_id')) {
$query->where('tenant_id', $tenantId);
}
});
// 自动设置租户ID
static::event('before_insert', function ($model) {
$tenantId = app('request')->tenantId ?? null;
if ($tenantId && in_array('tenant_id', $model->getTableFields())) {
$model->tenant_id = $tenantId;
}
});
}
// 移除租户过滤(用于跨租户查询)
public function withoutTenantScope()
{
return $this->withoutGlobalScope('tenant');
}
}
4.2 业务模型继承示例
<?php
namespace appmodel;
class User extends BaseModel
{
protected $table = 'users';
public function tenant()
{
return $this->belongsTo(Tenant::class);
}
}
五、完整实战:多租户CRM系统
基于上述架构实现一个简易的多租户客户关系管理系统。
5.1 控制器实现
<?php
namespace appcontroller;
use appBaseController;
use appmodelCustomer;
class CustomerController extends BaseController
{
// 获取当前租户的客户列表
public function index()
{
$page = $this->request->param('page', 1);
$size = $this->request->param('size', 15);
$customers = Customer::withSearch(['name', 'email'], [
'name' => $this->request->param('name'),
'email' => $this->request->param('email')
])->paginate(['page' => $page, 'list_rows' => $size]);
return json([
'code' => 200,
'data' => $customers->items(),
'total' => $customers->total()
]);
}
// 创建客户(自动绑定当前租户)
public function create()
{
$data = $this->request->only(['name', 'email', 'phone']);
$customer = new Customer();
$result = $customer->save($data);
if ($result) {
return json(['code' => 200, 'message' => '客户创建成功']);
} else {
return json(['code' => 500, 'message' => '客户创建失败']);
}
}
}
5.2 模型搜索器实现
<?php
namespace appmodel;
class Customer extends BaseModel
{
protected $table = 'customers';
// 定义搜索器
public function searchNameAttr($query, $value)
{
$value && $query->where('name', 'like', '%' . $value . '%');
}
public function searchEmailAttr($query, $value)
{
$value && $query->where('email', 'like', '%' . $value . '%');
}
}
5.3 路由配置
// route/app.php
use thinkfacadeRoute;
Route::group('customer', function () {
Route::get('list', 'CustomerController/index');
Route::post('create', 'CustomerController/create');
Route::put('update/:id', 'CustomerController/update');
Route::delete('delete/:id', 'CustomerController/delete');
});
六、高级特性与优化建议
6.1 租户数据库动态连接
// 动态切换租户独立数据库
public static function switchTenantDatabase($tenant)
{
if ($tenant->database_name) {
$config = [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => $tenant->database_name,
'username' => 'root',
'password' => 'password',
'charset' => 'utf8mb4',
];
Db::connect($config, 'tenant_' . $tenant->id);
}
}
6.2 性能优化策略
- 使用Redis缓存租户配置信息
- 实现数据库连接池管理
- 添加租户级别的查询缓存
- 定期清理无效租户数据
七、部署与测试
完整的测试用例确保多租户隔离的正确性。
7.1 单元测试示例
<?php
namespace tests;
use thinktestingTestCase;
class TenantTest extends TestCase
{
public function testTenantIsolation()
{
// 模拟租户A请求
$this->withHeader('Host', 'companya.example.com')
->get('/customer/list');
$customersA = Customer::select();
$this->assertTrue($customersA->every(function ($item) {
return $item->tenant_id == 1;
}));
// 模拟租户B请求
$this->withHeader('Host', 'companyb.example.com')
->get('/customer/list');
$customersB = Customer::select();
$this->assertTrue($customersB->every(function ($item) {
return $item->tenant_id == 2;
}));
}
}

