本文目录导读:

- 基于中间件/拦截器的路由(Laravel 示例)
- 基于模型事件的动态路由(Laravel)
- 自定义数据库管理器(ThinkPHP 示例)
- 基于读写分离的路由
- 高级方案:连接池 + 路由表
- 最佳实践与注意事项
- 完整示例框架(Laravel)
- 使用 Laravel 包简化开发
在 PHP 中实现动态数据源路由,通常是为了支持多租户架构、读写分离或分库分表,以下是几种常见的实现方案,从简单到复杂:
基于中间件/拦截器的路由(Laravel 示例)
最常见的做法是在请求生命周期中根据条件切换数据库连接。
实现方案
<?php
// app/Http/Middleware/DatabaseRouterMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
class DatabaseRouterMiddleware
{
public function handle($request, Closure $next)
{
// 1. 根据子域名、请求头或参数判断租户
$tenant = $request->header('X-Tenant-ID')
?? $request->input('tenant_id')
?? explode('.', $request->getHost())[0];
// 2. 动态配置数据库连接
Config::set('database.connections.tenant.host', '192.168.1.' . $tenant);
Config::set('database.connections.tenant.database', "tenant_db_{$tenant}");
Config::set('database.connections.tenant.username', 'tenant_user_' . $tenant);
Config::set('database.connections.tenant.password', 'secret_' . $tenant);
// 3. 设置默认连接(或通过 DB::connection('tenant') 使用)
DB::setDefaultConnection('tenant');
return $next($request);
}
}
使用方式:
// 在控制器中
$users = DB::table('users')->get(); // 自动使用租户连接
// 或
$users = DB::connection('tenant')->table('users')->get();
基于模型事件的动态路由(Laravel)
通过模型的事件监听实现自动切换。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class BaseModel extends Model
{
protected static function boot()
{
parent::boot();
static::retrieved(function ($model) {
// 查询后自动切回默认连接
DB::setDefaultConnection('mysql');
});
}
// 自定义动态路由方法
public function setConnection($name)
{
// 根据业务逻辑动态设置连接
if ($this->tenant_id == 1) {
$name = 'tenant_a';
} elseif ($this->tenant_id == 2) {
$name = 'tenant_b';
}
return parent::setConnection($name);
}
}
自定义数据库管理器(ThinkPHP 示例)
ThinkPHP 允许更灵活的多数据库配置。
<?php
// config/database.php 返回一个数组
return [
// 默认连接
'default' => 'mysql',
// 数据库配置
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => '127.0.0.1',
'database' => 'main_db',
'username' => 'root',
'password' => '',
],
// 动态添加的租户连接
'tenant_1' => [
'type' => 'mysql',
'hostname' => '192.168.1.101',
'database' => 'db_tenant_1',
'username' => 'tenant_1',
'password' => 'pass_1',
],
],
];
动态路由示例:
<?php
namespace app\middleware;
use think\facade\Config;
use think\facade\Db;
class TenantRouter
{
public function handle($request, \Closure $next)
{
$tenantId = $request->header('X-Tenant-ID');
if ($tenantId) {
// 动态添加连接配置
Config::set("database.connections.tenant_{$tenantId}", [
'type' => 'mysql',
'hostname' => "db-server-{$tenantId}.internal.com",
'database' => "app_{$tenantId}",
'username' => "user_{$tenantId}",
'password' => 'secret'
]);
// 切换当前连接
Db::setConfig([
'connections' => [
'default' => "tenant_{$tenantId}"
]
]);
}
return $next($request);
}
}
基于读写分离的路由
实现自动的读写分离。
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
class DatabaseRouter
{
private static $readConnections = [
'mysql_read_1' => ['host' => '192.168.1.10', 'database' => 'app_read'],
'mysql_read_2' => ['host' => '192.168.1.11', 'database' => 'app_read'],
];
private static $writeConnection = [
'host' => '192.168.1.20',
'database' => 'app_write',
'username' => 'writer',
'password' => 'write_pass'
];
public static function configure()
{
// 配置写连接
Config::set('database.connections.mysql_write', self::$writeConnection);
// 配置读连接(使用负载均衡)
$readConfig = self::$readConnections;
Config::set('database.connections.mysql_read', [
'driver' => 'mysql',
'read' => [
['host' => $readConfig['mysql_read_1']['host']],
['host' => $readConfig['mysql_read_2']['host']],
],
'write' => [
'host' => $readConfig['mysql_read_1']['host'],
],
'database' => $readConfig['mysql_read_1']['database'],
'username' => 'reader',
'password' => 'read_pass',
]);
}
public static function getConnection($needWrite = false)
{
self::configure();
if ($needWrite) {
DB::setDefaultConnection('mysql_write');
} else {
DB::setDefaultConnection('mysql_read');
}
}
}
// 使用
DatabaseRouter::getConnection(true); // 写入操作
$user = User::create([...]);
DatabaseRouter::getConnection(); // 读取操作
$users = User::where('status', 1)->get();
高级方案:连接池 + 路由表
使用缓存存储租户与数据库的映射关系。
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
class MultiTenantRouter
{
public static function switch($tenantId)
{
// 从缓存获取路由配置
$route = Cache::remember("tenant_route_{$tenantId}", 3600, function () use ($tenantId) {
// 从主库查询租户数据库位置
return DB::connection('mysql_central')
->table('tenant_routes')
->where('tenant_id', $tenantId)
->first();
});
if (!$route) {
throw new \Exception("租户不存在: {$tenantId}");
}
// 动态配置连接
$connectionName = "tenant_{$tenantId}";
Config::set("database.connections.{$connectionName}", [
'driver' => 'mysql',
'host' => $route->db_host,
'port' => $route->db_port ?? 3306,
'database' => $route->db_name,
'username' => $route->db_user,
'password' => decrypt($route->db_password),
'charset' => 'utf8mb4',
]);
// 切换默认连接
DB::setDefaultConnection($connectionName);
return $connectionName;
}
public static function reset()
{
DB::setDefaultConnection('mysql_central');
}
}
最佳实践与注意事项
1 连接名称标准化
- 使用统一的命名规则:
tenant_{id}或{type}_{id} - 避免连接名冲突
2 缓存优化
// 路由表缓存
Cache::tags(['tenant_router'])->remember("connection_{$tenantId}", 600, function () {
// ...
});
3 错误处理
try {
DB::connection('tenant_123')->table('users')->get();
} catch (\Exception $e) {
// 切换失败回退到默认连接
DB::setDefaultConnection('mysql');
Log::error('Database routing failed', ['tenant' => $tenantId]);
}
4 连接关闭
// 请求结束或任务完成后清理
public function terminate($request, $response)
{
DB::disconnect($this->connectionName);
}
完整示例框架(Laravel)
<?php
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
//
}
public function boot()
{
// 监听查询事件,实现自动路由
DB::listen(function ($query) {
// 如果当前连接不是默认的,可以在这里处理
if ($query->connectionName !== config('database.default')) {
// ... 处理特定连接
}
});
}
}
使用 Laravel 包简化开发
如果你不想自己实现,可以使用成熟的包:
- tenancy/tenancy - 多租户解决方案
- stancl/tenancy - Laravel 多租户包
- spatie/laravel-multitenancy - 轻量级多租户
安装示例(stancl/tenancy):
composer require stancl/tenancy php artisan tenancy:install php artisan tenancy:migrate
选择哪种方案取决于你的需求:
| 场景 | 推荐方案 |
|---|---|
| 简单读写分离 | 中间件配置数据库连接 |
| 多租户 SaaS | 基于子域名/Header 路由 |
| 高并发读写 | 连接池 + 读写分离 |
| 复杂分库分表 | 使用成熟多租户包 |
核心要点:
- 通过配置动态修改数据库连接
- 使用统一的连接名称管理
- 做好连接的错误回退
- 考虑性能(使用缓存路由表)