免费资源下载
从零构建企业级多租户应用,掌握数据库隔离与数据权限的核心技术
多租户架构设计模式解析
在SaaS系统开发中,多租户架构是核心技术挑战。ThinkPHP 6.x提供了灵活的扩展机制,支持三种主流的多租户实现方案。
1. 独立数据库模式
- 每个租户拥有独立数据库
- 数据隔离级别最高
- 适合大型企业客户
- 运维成本相对较高
2. 共享数据库独立Schema
- 同一数据库不同数据表
- 平衡隔离与成本
- 适合中型SaaS应用
- 需要动态表名支持
3. 共享数据库共享表
- 通过tenant_id字段区分
- 成本最低实现最简单
- 适合初创SaaS项目
- 数据隔离依赖程序逻辑
实战案例:共享数据库共享表模式实现
我们将采用第三种方案,通过中间件和模型基类实现数据自动隔离,这是最适合快速启动项目的方案。
Step 1: 数据库设计
-- 租户表
CREATE TABLE `tenants` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`tenant_code` varchar(50) NOT NULL COMMENT '租户标识码',
`name` varchar(100) NOT NULL COMMENT '租户名称',
`status` tinyint(1) DEFAULT 1 COMMENT '状态:1-正常 0-禁用',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_code` (`tenant_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户表(包含tenant_id字段)
CREATE TABLE `users` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`tenant_id` int(11) UNSIGNED NOT NULL COMMENT '租户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`email` varchar(100) NOT NULL COMMENT '邮箱',
`password` varchar(255) NOT NULL COMMENT '密码',
`is_admin` tinyint(1) DEFAULT 0 COMMENT '是否管理员',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`),
UNIQUE KEY `uk_tenant_username` (`tenant_id`, `username`),
UNIQUE KEY `uk_tenant_email` (`tenant_id`, `email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 其他业务表都需要包含tenant_id字段
CREATE TABLE `products` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`tenant_id` int(11) UNSIGNED NOT NULL COMMENT '租户ID',
`name` varchar(200) NOT NULL COMMENT '产品名称',
`price` decimal(10,2) NOT NULL COMMENT '价格',
`stock` int(11) DEFAULT 0 COMMENT '库存',
`status` tinyint(1) DEFAULT 1 COMMENT '状态',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Step 2: 创建租户识别中间件
<?php
// app/middleware/TenantIdentify.php
namespace appmiddleware;
use thinkfacadeDb;
use thinkfacadeSession;
class TenantIdentify
{
public function handle($request, Closure $next)
{
// 从子域名识别租户(如:company1.saasapp.com)
$host = $request->host();
$subdomain = $this->extractSubdomain($host);
// 或者从请求头识别(适合移动端API)
$tenantCode = $request->header('X-Tenant-Code')
?: $request->param('tenant_code')
?: $subdomain;
if (!$tenantCode) {
// 跳转到主域名或显示租户选择页面
return redirect('/select-tenant');
}
// 查询租户信息
$tenant = Db::name('tenants')
->where('tenant_code', $tenantCode)
->where('status', 1)
->find();
if (!$tenant) {
return json(['code' => 404, 'msg' => '租户不存在或已禁用']);
}
// 检查租户是否过期
if ($tenant['expire_time'] && strtotime($tenant['expire_time'])
Step 3: 创建多租户模型基类
<?php
// app/model/TenantModel.php
namespace appmodel;
use thinkModel;
use thinkfacadeSession;
abstract class TenantModel extends Model
{
// 自动写入租户ID
protected $autoWriteTenantId = true;
// 租户ID字段名
protected $tenantField = 'tenant_id';
protected static function onBeforeInsert(Model $model)
{
if ($model->autoWriteTenantId) {
$tenantId = Session::get('tenant_id');
if ($tenantId && empty($model->getData($model->tenantField))) {
$model->set($model->tenantField, $tenantId);
}
}
}
protected static function onBeforeUpdate(Model $model)
{
// 更新时确保不修改租户ID
if ($model->autoWriteTenantId) {
$model->readonly($model->tenantField);
}
}
protected static function onBeforeSelect(Model $model)
{
// 自动添加租户查询条件
if ($model->autoWriteTenantId) {
$tenantId = Session::get('tenant_id');
if ($tenantId) {
$model->where($model->getTable() . '.' . $model->tenantField, $tenantId);
}
}
}
/**
* 获取当前租户的所有数据(管理员权限)
*/
public function scopeTenant($query, $tenantId = null)
{
$tenantId = $tenantId ?: Session::get('tenant_id');
return $query->where($this->tenantField, $tenantId);
}
/**
* 临时关闭租户过滤(用于超级管理员)
*/
public function withoutTenantScope()
{
$this->autoWriteTenantId = false;
return $this;
}
}
Step 4: 业务模型实现
<?php
// app/model/Product.php
namespace appmodel;
class Product extends TenantModel
{
// 定义数据表名
protected $table = 'products';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = 'updated_at';
// 定义字段类型
protected $type = [
'price' => 'float',
'stock' => 'integer',
'status' => 'integer'
];
// 搜索器:按名称搜索
public function searchNameAttr($query, $value)
{
return $query->whereLike('name', '%' . $value . '%');
}
// 搜索器:按价格范围搜索
public function searchPriceRangeAttr($query, $value)
{
if (isset($value[0])) {
$query->where('price', '>=', $value[0]);
}
if (isset($value[1])) {
$query->where('price', ' Request::param('name'),
'priceRange' => [Request::param('min_price'), Request::param('max_price')]
])
->order('id', 'desc')
->paginate([
'page' => $page,
'list_rows' => $size
]);
return json([
'code' => 200,
'data' => $products->items(),
'total' => $products->total(),
'current_page' => $products->currentPage()
]);
}
// 创建产品(自动添加tenant_id)
public function create()
{
$data = Request::only(['name', 'price', 'stock', 'status']);
$product = new Product();
$result = $product->save($data);
if ($result) {
return json(['code' => 200, 'msg' => '创建成功', 'data' => $product]);
} else {
return json(['code' => 500, 'msg' => '创建失败']);
}
}
// 更新产品(确保只能更新当前租户的数据)
public function update($id)
{
$product = Product::find($id);
if (!$product) {
return json(['code' => 404, 'msg' => '产品不存在']);
}
$data = Request::only(['name', 'price', 'stock', 'status']);
$result = $product->save($data);
if ($result) {
return json(['code' => 200, 'msg' => '更新成功']);
} else {
return json(['code' => 500, 'msg' => '更新失败']);
}
}
// 删除产品
public function delete($id)
{
$product = Product::find($id);
if (!$product) {
return json(['code' => 404, 'msg' => '产品不存在']);
}
// 软删除(如果模型支持)
$result = $product->delete();
if ($result) {
return json(['code' => 200, 'msg' => '删除成功']);
} else {
return json(['code' => 500, 'msg' => '删除失败']);
}
}
}
高级功能:租户数据隔离与权限控制
1. 动态数据库连接
// 根据租户动态切换数据库配置
$tenant = $request->tenant;
if ($tenant['database_type'] === 'independent') {
$config = [
'type' => 'mysql',
'hostname' => $tenant['db_host'],
'database' => $tenant['db_name'],
'username' => $tenant['db_user'],
'password' => $tenant['db_pass'],
'hostport' => $tenant['db_port']
];
Db::connect($config, 'tenant_' . $tenant['id']);
}
2. Redis多租户隔离
// 使用不同的Redis数据库或前缀
$tenantId = Session::get('tenant_id');
$redis = new Redis();
$redis->select($tenantId); // 选择不同的DB
// 或者使用前缀
$cacheKey = 'tenant:' . $tenantId . ':user:' . $userId;
$redis->set($cacheKey, $userData);
3. 文件存储隔离方案
// 配置文件存储路径
// config/filesystem.php
return [
'default' => 'local',
'disks' => [
'local' => [
'type' => 'local',
'root' => app()->getRootPath() . 'storage',
],
'tenant' => [
'type' => 'local',
'root' => function() {
$tenantId = Session::get('tenant_id');
return app()->getRootPath() . 'storage/tenants/' . $tenantId;
}
],
]
];
// 使用租户隔离的文件存储
$file = request()->file('image');
$saveName = thinkfacadeFilesystem::disk('tenant')
->putFile('uploads', $file);
性能优化与监控
数据库优化
- 为tenant_id字段建立索引
- 使用数据库连接池
- 定期清理过期租户数据
- 分表策略应对大数据量
缓存策略
- 租户级缓存前缀隔离
- 热点数据预加载
- 缓存雪崩防护机制
- 多级缓存架构
监控告警
- 租户API调用统计
- 数据库连接数监控
- 慢查询日志分析
- 自动扩缩容机制
部署与运维指南
Nginx配置示例
server {
listen 80;
server_name ~^(?<subdomain>.+).saasapp.com$;
root /var/www/saasapp/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_HOST $host;
include fastcgi_params;
# 传递子域名到PHP
fastcgi_param X_TENANT_CODE $subdomain;
}
}
Docker Compose配置
version: '3.8'
services:
app:
build: .
volumes:
- ./:/var/www/html
environment:
- APP_ENV=production
- APP_DEBUG=false
- TENANT_MODE=shared
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx.conf:/etc/nginx/nginx.conf
- ./public:/var/www/html/public
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: saas_main
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:alpine
command: redis-server --appendonly yes
volumes:
mysql_data:
安全注意事项
- SQL注入防护:确保所有查询都使用参数绑定,ThinkPHP的ORM已提供基础防护
- 跨租户数据泄露:严格验证每个数据操作的租户权限,避免查询条件被绕过
- 会话隔离:不同租户使用不同的会话存储,避免会话数据混淆
- 文件上传安全:限制文件类型和大小,使用租户隔离的存储路径
- API限流:为每个租户设置独立的API调用频率限制
- 数据备份:制定租户数据的定期备份和恢复策略

