跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

如何在Hyperf中实现多租户数据库隔离_基于数据源动态切换插件

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
中取出来。不要试图在中间件里“切换”已存在的连接,那是无效的。 Hyperf 3.1.66 本页面提供企业级 PHP 协程框架 Hyperf 3.1.66 版本的官方源码下载与完整更新日志。重点解析 v3.1.66 版本中新增的 gRPC 多客户端负载均衡支持、Pool 连接池全量刷新、Guzzle 持久化 Cookie 以及数据库 JSON 包含键查询等核心优化特性。 下载 步骤一:定义一个
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,否则后台任务永远走默认库

相关文章