hyperf 默认不支持多租户数据源动态切换,因 dbconnection 在容器启动时即初始化并绑定到 db,无法感知请求上下文中的租户标识;必须替换为继承 connection 的代理类 tenantdbconnection,在调用时按 tenant_id 动态解析对应数据源。

Hyperf 默认不支持多租户数据源动态切换,必须自己接管 DbConnection 的创建和路由逻辑,否则所有请求都会走默认配置的连接。
为什么不能直接用 Hyperf\Database\Connection 的默认实例
Hyperf 的 DbConnection 在容器启动时就完成初始化,并绑定到 db 这个 ID 上;它不会感知请求上下文里的租户标识,也不会在每次查询前重新解析数据库配置。一旦你注册了多个数据源(比如 db.tenant_a、db.tenant_b),它们只是“存在”,但框架仍只会用 db 对应的那个连接。
- 现象:你在中间件里设置了
tenant_id = 'a',但Db::select()依然查的是主库 - 根本原因:DB facade 底层调用的是容器中固定的
DbConnection实例,不是按需构造的 - 绕不开的点:必须替换掉容器中
Hyperf\Database\Connection的原始定义,换成一个能根据租户动态选源的代理类
如何实现运行时数据源路由代理
核心是写一个继承自 Hyperf\Database\Connection 的子类,在构造时延迟加载真实连接,并把租户 ID 从 RequestContext 或 ApplicationContext 中取出来。不要试图在中间件里“切换”已存在的连接,那是无效的。
- 步骤一:定义一个
TenantDbConnection类,重写__call()和关键方法(如select()、table()),内部通过$this->getConnection()拿当前租户的真实连接 - 步骤二:在
dependencies.php中覆盖原Hyperf\Database\Connection绑定:return [ Hyperf\Database\Connection::class => \App\Tenant\TenantDbConnection::class, ]; - 步骤三:在
TenantDbConnection构造函数里不立即初始化 PDO,而是缓存$tenantId,并在首次调用查询方法时才调用$this->resolveConnection()去容器里 get 对应的db.tenant_x - 注意:每个租户的数据源必须提前在
config/autoload/databases.php里声明,例如'db.tenant_1' => [/* config */],否则make()会失败
租户标识从哪来?别依赖全局变量或静态属性
Hyperf 是协程环境,$_SERVER、全局变量、静态属性都可能跨请求污染。必须用协程安全的方式传递租户上下文。
- 推荐方案:在网关或第一个中间件里,从 header(如
X-Tenant-ID)、域名(tenant-a.example.com)或 JWT payload 解析出tenant_id,然后存入CoroutineContext:use Hyperf\Context\Context; Context::set('tenant_id', $tenantId); - 禁止做法:用
static $tenantId缓存,或在__construct()里直接读$_SERVER—— 协程切换后值就不可靠 - 验证方式:在
TenantDbConnection::getConnection()里加日志,确认每次拿到的tenant_id和当前请求一致
性能与事务边界容易被忽略的点
动态切换连接本身开销不大,但如果你在一次请求里混用多个租户的查询,或者在一个事务里跨租户操作,就会出问题。
- 事务无法跨数据源:
Db::transaction()只对单个Connection生效,切到另一个租户连接后,事务上下文就断了 - 连接池复用失效:每个租户连接都是独立的
Connection实例,对应独立的连接池,要注意max_connections配置是否足够 - 缓存键没隔离:如果用了
Db::table()->cache(),记得把tenant_id加进 cache key,否则 A 租户查的数据可能被 B 租户命中 - 最麻烦的情况:队列任务里没有
RequestContext,必须显式传入tenant_id并手动 set 到 Context,否则后台任务永远走默认库











