本文目录导读:

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);
}
}
最佳实践建议
-
使用容器管理连接依赖
- 不要到处直接创建连接
- 使用依赖注入框架管理
-
合理设计分片策略
// 考虑租户数据量,选择合适的算法 function getShardId(int $tenantId): int { // 支持范围分片 if ($tenantId < 1000) return 1; if ($tenantId < 2000) return 2; // 或哈希分片 return $tenantId % 10; } -
实现故障转移
class FailoverManager { public function getConnection(int $tenantId): PDO { try { return TenantDatabaseManager::getConnection($tenantId); } catch (PDOException $e) { // 切换到备用数据库 return $this->getBackupConnection($tenantId); } } } -
监控和日志
- 记录每次租户切换
- 监控连接池使用情况
- 追踪分布式事务
这些方案可以根据你的实际需求进行组合和调整,关键是建立统一的租户上下文管理机制,并在应用层面做好抽象和封装。