免费资源下载
作者:技术架构师 | 发布日期:2023年10月
一、多租户架构设计概述
在当今云原生时代,SaaS(软件即服务)模式已成为企业应用的主流部署方式。多租户架构允许单个应用实例为多个客户(租户)提供服务,同时确保数据隔离和安全性。本文将基于ThinkPHP 6.0框架,详细讲解三种主流的多租户实现方案:
- 独立数据库方案:每个租户拥有独立的数据库实例
- 共享数据库独立Schema:共享数据库但使用不同的数据表结构
- 共享数据表方案:所有租户共享数据表,通过tenant_id字段区分
我们将重点讲解第三种方案的实现,因其在资源利用和运维复杂度间取得最佳平衡。
二、环境准备与项目初始化
# 创建ThinkPHP 6.0项目
composer create-project topthink/think saas-project
cd saas-project
# 安装多租户扩展包
composer require hyn/multi-tenant
# 创建数据库
CREATE DATABASE saas_platform DEFAULT CHARACTER SET utf8mb4;
# 生成租户管理相关表
php think migrate:run
三、核心数据库设计
3.1 租户信息表(tenants)
CREATE TABLE `tenants` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`tenant_code` varchar(50) NOT NULL COMMENT '租户唯一标识',
`company_name` varchar(100) NOT NULL COMMENT '公司名称',
`domain` varchar(100) DEFAULT NULL COMMENT '绑定域名',
`database_name` varchar(50) DEFAULT NULL COMMENT '独立数据库名',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:1-正常 0-禁用',
`expire_time` datetime DEFAULT NULL COMMENT '服务到期时间',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_tenant_code` (`tenant_code`),
UNIQUE KEY `uniq_domain` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 用户表(users)增加租户标识
ALTER TABLE `users`
ADD COLUMN `tenant_id` INT UNSIGNED NOT NULL AFTER `id`,
ADD INDEX `idx_tenant` (`tenant_id`);
四、多租户中间件实现
创建租户识别中间件,自动识别当前请求所属租户:
<?php
namespace appmiddleware;
use thinkfacadeDb;
use thinkfacadeRequest;
class TenantIdentify
{
public function handle($request, Closure $next)
{
// 方案1:通过子域名识别 tenant1.example.com
$subdomain = explode('.', Request::host())[0];
// 方案2:通过请求头识别
$tenantHeader = $request->header('X-Tenant-Identifier');
// 方案3:通过URL参数识别(开发环境)
$tenantParam = $request->param('tenant_code');
$tenantCode = $tenantHeader ?: ($subdomain !== 'www' ? $subdomain : $tenantParam);
if ($tenantCode) {
$tenant = Db::name('tenants')
->where('tenant_code', $tenantCode)
->where('status', 1)
->find();
if ($tenant) {
// 设置租户上下文
$request->tenant = $tenant;
// 动态切换数据库(如需)
if ($tenant['database_name']) {
$this->switchDatabase($tenant['database_name']);
}
// 设置全局查询条件
$this->addGlobalScope($tenant['id']);
}
}
return $next($request);
}
private function switchDatabase($databaseName)
{
config('database.connections.tenant', [
'type' => 'mysql',
'hostname' => config('database.hostname'),
'database' => $databaseName,
// ... 其他配置
]);
Db::connect('tenant');
}
private function addGlobalScope($tenantId)
{
// 全局自动添加tenant_id条件
thinkfacadeDb::event('before_select', function($query) use ($tenantId) {
if (in_array('tenant_id', $query->getTableFields())) {
$query->where('tenant_id', $tenantId);
}
});
}
}
在middleware.php中注册中间件:
return [
// 全局中间件
appmiddlewareTenantIdentify::class,
];
五、业务逻辑层封装
5.1 基础模型类封装
<?php
namespace appcommonmodel;
use thinkModel;
class BaseModel extends Model
{
// 自动写入租户ID
protected $autoWriteTimestamp = true;
protected static function onBeforeInsert(Model $model)
{
if (request()->tenant && in_array('tenant_id', $model->getTableFields())) {
$model->tenant_id = request()->tenant['id'];
}
}
/**
* 获取当前租户的查询实例
*/
public function scopeTenant($query)
{
if (request()->tenant && in_array('tenant_id', $this->getTableFields())) {
$query->where('tenant_id', request()->tenant['id']);
}
return $query;
}
}
5.2 业务模型示例
<?php
namespace appmodel;
use appcommonmodelBaseModel;
class Product extends BaseModel
{
protected $table = 'products';
// 自动关联租户
public function tenant()
{
return $this->belongsTo(Tenant::class);
}
/**
* 获取当前租户的所有产品
*/
public static function getTenantProducts($page = 1, $limit = 15)
{
return self::tenant()
->with(['category'])
->order('created_at', 'desc')
->paginate([
'page' => $page,
'list_rows' => $limit
]);
}
}
六、租户数据隔离实战案例
6.1 订单模块实现
<?php
namespace appcontroller;
use appBaseController;
use appmodelOrder;
use appmodelTenant;
class OrderController extends BaseController
{
/**
* 创建订单(自动绑定租户)
*/
public function create()
{
$data = $this->request->post();
// 自动添加租户信息
$data['tenant_id'] = $this->request->tenant['id'];
$data['order_no'] = $this->generateOrderNo();
$order = Order::create($data);
// 记录租户操作日志
$this->logTenantAction('order_create', [
'order_id' => $order->id,
'amount' => $order->amount
]);
return json([
'code' => 200,
'msg' => '订单创建成功',
'data' => $order
]);
}
/**
* 生成租户唯一的订单号
*/
private function generateOrderNo()
{
$tenantCode = $this->request->tenant['tenant_code'];
$date = date('YmdHis');
$random = mt_rand(1000, 9999);
return "{$tenantCode}{$date}{$random}";
}
/**
* 获取当前租户的订单统计
*/
public function statistics()
{
$tenantId = $this->request->tenant['id'];
$stats = Order::tenant()
->field([
'COUNT(*) as total_orders',
'SUM(amount) as total_amount',
'AVG(amount) as avg_amount',
'COUNT(DISTINCT user_id) as unique_customers'
])
->whereTime('created_at', 'month')
->find();
return json($stats);
}
}
七、高级特性:数据库连接池优化
对于大规模多租户应用,数据库连接管理至关重要:
<?php
namespace appcommonlib;
use thinkfacadeDb;
use SwooleDatabasePDOPool;
class TenantConnectionPool
{
private static $pools = [];
/**
* 获取租户数据库连接
*/
public static function getConnection($tenantId)
{
if (!isset(self::$pools[$tenantId])) {
$config = self::getTenantDbConfig($tenantId);
self::$pools[$tenantId] = new PDOPool(
$config,
env('database.pool_size', 10)
);
}
return self::$pools[$tenantId]->get();
}
/**
* 释放连接
*/
public static function releaseConnection($tenantId, $connection)
{
if (isset(self::$pools[$tenantId])) {
self::$pools[$tenantId]->put($connection);
}
}
/**
* 动态获取租户数据库配置
*/
private static function getTenantDbConfig($tenantId)
{
// 可从缓存或配置中心获取
$tenantConfig = Db::name('tenant_configs')
->where('tenant_id', $tenantId)
->find();
return [
'host' => $tenantConfig['db_host'] ?? config('database.hostname'),
'port' => $tenantConfig['db_port'] ?? config('database.hostport'),
'dbname' => $tenantConfig['database_name'],
'charset' => 'utf8mb4',
'username' => $tenantConfig['db_user'] ?? config('database.username'),
'password' => $tenantConfig['db_password'] ?? config('database.password')
];
}
}
八、安全与权限控制
8.1 租户数据越权访问防护
<?php
namespace appmiddleware;
class TenantDataGuard
{
public function handle($request, Closure $next)
{
$response = $next($request);
// 验证返回数据是否属于当前租户
if ($request->tenant && $response->getData()) {
$data = $response->getData();
if (is_array($data) && isset($data['data'])) {
$this->validateTenantData($data['data'], $request->tenant['id']);
}
}
return $response;
}
private function validateTenantData($data, $tenantId)
{
if (is_array($data)) {
// 如果是列表数据
if (isset($data[0]) && is_array($data[0])) {
foreach ($data as $item) {
if (isset($item['tenant_id']) && $item['tenant_id'] != $tenantId) {
throw new Exception('数据权限验证失败');
}
}
}
// 如果是单条数据
elseif (isset($data['tenant_id']) && $data['tenant_id'] != $tenantId) {
throw new Exception('数据权限验证失败');
}
}
}
}
九、性能优化建议
- 查询缓存策略:为每个租户建立独立的缓存前缀,避免缓存污染
- 数据库索引优化:在所有tenant_id字段上建立索引,提升查询性能
- 连接复用:使用数据库连接池减少连接创建开销
- 分库分表:当单个租户数据量过大时,考虑按时间分表
- 异步处理:租户的批量操作采用队列异步执行
缓存键设计示例:
// 租户隔离的缓存键
$cacheKey = "tenant:{$tenantId}:products:{$productId}";
// Redis缓存示例
$redis = thinkfacadeCache::store('redis');
$redis->set($cacheKey, $productData, 3600);
十、部署与监控
10.1 环境变量配置
# .env 多租户配置
TENANT_MODE=shared_database
TENANT_ID_COLUMN=tenant_id
MAX_TENANTS_PER_SERVER=1000
TENANT_CACHE_DRIVER=redis
10.2 健康检查接口
<?php
namespace appcontroller;
class HealthController
{
/**
* 租户系统健康检查
*/
public function check()
{
$tenantId = request()->tenant['id'] ?? 0;
$status = [
'tenant_id' => $tenantId,
'database' => $this->checkDatabase($tenantId),
'cache' => $this->checkCache(),
'storage' => $this->checkStorage($tenantId),
'timestamp' => time()
];
return json($status);
}
}

