PHP 分库多租户怎么切

wen PHP项目 2

本文目录导读:

PHP 分库多租户怎么切

  1. 核心方案概览
  2. 高级实现方案
  3. 进阶优化方案
  4. 具体应用示例
  5. 最佳实践建议

PHP 分库多租户的切换是实现多租户架构的核心问题,根据不同业务场景,主要有以下几种方案,我来详细说明:

核心方案概览

按租户ID分库(最常见)

class TenantDatabaseManager
{
    private static $connections = [];
    public static function getConnection(int $tenantId): PDO
    {
        // 根据租户ID计算数据库分片
        $dbIndex = $tenantId % 10; // 假设分10个库
        if (!isset(self::$connections[$dbIndex])) {
            $config = [
                'host' => "db_{$dbIndex}.example.com",
                'dbname' => "tenant_db_{$dbIndex}",
                'user' => "tenant_user_{$dbIndex}",
                'pass' => "password_{$dbIndex}"
            ];
            self::$connections[$dbIndex] = $this->createConnection($config);
        }
        return self::$connections[$dbIndex];
    }
}

动态切换数据库连接

class MultiTenantService
{
    private $connection;
    private $currentTenant;
    public function switchTenant(int $tenantId): void
    {
        $this->currentTenant = $tenantId;
        $this->connection = $this->createTenantConnection($tenantId);
    }
    private function createTenantConnection(int $tenantId): PDO
    {
        $config = $this->getTenantConfig($tenantId);
        $dsn = "mysql:host={$config['host']};dbname={$config['database']}";
        $pdo = new PDO($dsn, $config['username'], $config['password']);
        return $pdo;
    }
}

高级实现方案

基于中间件的动态路由

class TenantRouter
{
    private $tenantConfigs = [];
    public function routeRequest(Request $request): void
    {
        $tenantId = $this->extractTenantId($request);
        // 使用中间件动态选择数据库
        $this->setActiveConnection($tenantId);
        // 使用容器管理依赖注入
        app()->bind(DB::class, function () use ($tenantId) {
            return $this->getTenantConnection($tenantId);
        });
    }
    private function extractTenantId(Request $request): int
    {
        // 从JWT、Session或Header中获取租户ID
        return (int) $request->header('X-Tenant-ID');
    }
}

Laravel框架实现(推荐)

// app/Providers/TenantServiceProvider.php
class TenantServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('tenant.manager', function ($app) {
            return new TenantManager();
        });
    }
    public function boot()
    {
        // 通过中间件切换数据库
        $this->app['router']->aliasMiddleware('tenant', TenantMiddleware::class);
    }
}
// app/Http/Middleware/TenantMiddleware.php
class TenantMiddleware
{
    public function handle($request, Closure $next)
    {
        $tenantId = $request->header('X-Tenant-ID', 1);
        // 动态配置数据库连接
        Config::set('database.connections.tenant', [
            'driver' => 'mysql',
            'host' => "db_" . ($tenantId % 10),
            'database' => "tenant_db_" . $tenantId,
            'username' => config('database.connections.mysql.username'),
            'password' => config('database.connections.mysql.password'),
        ]);
        // 切换默认连接
        DB::setDefaultConnection('tenant');
        return $next($request);
    }
}
// 使用示例
class UserController extends Controller
{
    public function index()
    {
        // 自动使用当前租户的数据库
        return User::all();
    }
}

进阶优化方案

连接池优化

class TenantConnectionPool
{
    private static $pool = [];
    private static $MAX_POOL_SIZE = 10;
    public static function getConnection(int $tenantId): PDO
    {
        $key = "tenant_{$tenantId}";
        if (!isset(self::$pool[$key]) || count(self::$pool[$key]) === 0) {
            // 创建新连接
            $connection = self::createConnection($tenantId);
        } else {
            // 从池中获取连接
            $connection = array_pop(self::$pool[$key]);
        }
        return $connection;
    }
    public static function returnConnection(PDO $connection, int $tenantId): void
    {
        $key = "tenant_{$tenantId}";
        if (!isset(self::$pool[$key])) {
            self::$pool[$key] = [];
        }
        if (count(self::$pool[$key]) < self::$MAX_POOL_SIZE) {
            self::$pool[$key][] = $connection;
        }
    }
}

缓存机制

class TenantConfigCache
{
    private static $cache = [];
    private static $ttl = 3600; // 1小时
    public static function getTenantConfig(int $tenantId): array
    {
        $cacheKey = "tenant_config_{$tenantId}";
        if (!isset(self::$cache[$cacheKey]) || 
            time() - self::$cache[$cacheKey]['time'] > self::$ttl) {
            // 从配置中心获取
            self::$cache[$cacheKey] = [
                'time' => time(),
                'config' => self::loadFromConfigCenter($tenantId)
            ];
        }
        return self::$cache[$cacheKey]['config'];
    }
}

批量操作处理

// 批量处理不同租户的数据
class TenantBatchProcessor
{
    public function processAllTenants(callable $callback)
    {
        $tenantIds = $this->getAllTenantIds();
        foreach ($tenantIds as $tenantId) {
            $this->switchTenant($tenantId);
            try {
                $result = $callback($tenantId);
                $this->logSuccess($tenantId);
            } catch (\Exception $e) {
                $this->logError($tenantId, $e);
                continue;
            }
        }
    }
}

具体应用示例

单一进程内切换

// 处理单个用户的请求
function handleUserRequest(int $userId)
{
    // 获取用户所属租户
    $tenantId = User::find($userId)->tenant_id;
    // 切换到租户数据库
    switchTenant($tenantId);
    // 执行业务逻辑
    $orders = Order::where('user_id', $userId)->get();
    // 操作完成后切换回默认数据库
    switchToDefault();
    return $orders;
}

基于Domain/子域名

class DomainTenantResolver
{
    public function resolveByDomain(string $domain): int
    {
        $domainMap = [
            'company1.example.com' => 1,
            'company2.example.com' => 2,
        ];
        return $domainMap[$domain] ?? 0;
    }
}

数据库读写分离增强

class TenantReadWriteManager
{
    public function getReadConnection(int $tenantId): PDO
    {
        // 使用从库进行读操作
        $config = $this->getTenantReadConfig($tenantId);
        return $this->createConnection($config);
    }
    public function getWriteConnection(int $tenantId): PDO
    {
        // 使用主库进行写操作
        return TenantDatabaseManager::getConnection($tenantId);
    }
}

最佳实践建议

  1. 使用容器管理连接依赖

    • 不要到处直接创建连接
    • 使用依赖注入框架管理
  2. 合理设计分片策略

    // 考虑租户数据量,选择合适的算法
    function getShardId(int $tenantId): int {
        // 支持范围分片
        if ($tenantId < 1000) return 1;
        if ($tenantId < 2000) return 2;
        // 或哈希分片
        return $tenantId % 10;
    }
  3. 实现故障转移

    class FailoverManager
    {
        public function getConnection(int $tenantId): PDO
        {
            try {
                return TenantDatabaseManager::getConnection($tenantId);
            } catch (PDOException $e) {
                // 切换到备用数据库
                return $this->getBackupConnection($tenantId);
            }
        }
    }
  4. 监控和日志

    • 记录每次租户切换
    • 监控连接池使用情况
    • 追踪分布式事务

这些方案可以根据你的实际需求进行组合和调整,关键是建立统一的租户上下文管理机制,并在应用层面做好抽象和封装。

抱歉,评论功能暂时关闭!