本文目录导读:

- 基于中间件/拦截器的路由(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 路由 |
| 高并发读写 | 连接池 + 读写分离 |
| 复杂分库分表 | 使用成熟多租户包 |
核心要点:
- 通过配置动态修改数据库连接
- 使用统一的连接名称管理
- 做好连接的错误回退
- 考虑性能(使用缓存路由表)
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播