ThinkPHP 6.0 实现多租户SaaS架构的完整实践指南 | 企业级应用开发

2026-01-05 0 704
免费资源下载

作者:技术架构师 | 发布日期: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('数据权限验证失败');
            }
        }
    }
}

九、性能优化建议

  1. 查询缓存策略:为每个租户建立独立的缓存前缀,避免缓存污染
  2. 数据库索引优化:在所有tenant_id字段上建立索引,提升查询性能
  3. 连接复用:使用数据库连接池减少连接创建开销
  4. 分库分表:当单个租户数据量过大时,考虑按时间分表
  5. 异步处理:租户的批量操作采用队列异步执行

缓存键设计示例:

// 租户隔离的缓存键
$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);
    }
}
ThinkPHP 6.0 实现多租户SaaS架构的完整实践指南 | 企业级应用开发
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 thinkphp ThinkPHP 6.0 实现多租户SaaS架构的完整实践指南 | 企业级应用开发 https://www.taomawang.com/server/thinkphp/1506.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务