PHP 动态数据源路由

wen PHP项目 2

本文目录导读:

PHP 动态数据源路由

  1. 基于中间件/拦截器的路由(Laravel 示例)
  2. 基于模型事件的动态路由(Laravel)
  3. 自定义数据库管理器(ThinkPHP 示例)
  4. 基于读写分离的路由
  5. 高级方案:连接池 + 路由表
  6. 最佳实践与注意事项
  7. 完整示例框架(Laravel)
  8. 使用 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 包简化开发

如果你不想自己实现,可以使用成熟的包:

安装示例(stancl/tenancy):

composer require stancl/tenancy
php artisan tenancy:install
php artisan tenancy:migrate

选择哪种方案取决于你的需求:

场景 推荐方案
简单读写分离 中间件配置数据库连接
多租户 SaaS 基于子域名/Header 路由
高并发读写 连接池 + 读写分离
复杂分库分表 使用成熟多租户包

核心要点:

  1. 通过配置动态修改数据库连接
  2. 使用统一的连接名称管理
  3. 做好连接的错误回退
  4. 考虑性能(使用缓存路由表)

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