Workerman不支持自动多租户数据库切换,需在中间件中完成租户识别、连接实例注入与协程上下文清理,避免连接残留污染后续请求。

Workerman 本身不提供多租户数据库切换能力,必须靠你自己在请求生命周期里手动控制连接实例的创建、绑定与销毁。核心难点不在“换库”,而在“换得干净”——协程复用下,一个连接残留会污染后续所有请求。
为什么不能直接改 config/database.php 里的 connection 配置
因为 Workerman 是常驻内存 + 协程模型,config/database.php 只在启动时加载一次,后续所有请求都共享同一份配置。你改了它,等于全局污染;而且 Laravel/ThinkPHP 风格的「连接标识」机制(如 Db::connect('tenant_123'))在 Workerman 的协程上下文中无法自动透传到 ORM 查询链路里。
- 修改配置后不重启进程,新值不会生效
- 即使 reload,也只影响新协程,老协程仍持旧连接
- ORM(如 Eloquent、Query Builder)默认走的是初始化时注册的 default connection,不会感知运行时变化
必须在中间件中完成租户识别 + 连接实例注入
租户识别必须早于任何 DB 查询、缓存读写、日志记录。推荐从 $_SERVER['HTTP_HOST'] 提取子域名(如 acme.example.com → acme),查主库确认租户状态后,立即构造专属连接实例并挂载到当前协程上下文。
- 用
TenantContext::setContext()把tenant_id和db_config写入协程本地存储(不是ThreadLocal,Workerman 用的是Co\Channel或AsyncLocalStorage) - 不要把连接实例存在全局变量或静态属性里,协程间会互相覆盖
- 务必在中间件末尾调用
TenantContext::destroy(),否则下一个请求可能复用上一个租户的连接 - 如果用了 PDO 连接池(如 webman-framework 默认),需绕过它——为每个租户新建独立
PDO实例,或重写ConnectionResolver
如何安全复用连接实例而不泄漏内存
频繁 new PDO 会耗尽连接数,但全量缓存又可能撑爆内存。折中方案是按租户名做弱引用缓存 + 设置 TTL。
- 用
WeakMap或Map缓存tenant_id → PDO映射,键是字符串,值是连接对象 - 每次获取前检查连接是否 alive:
$pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS) - 对长时间空闲的连接主动 close(比如超过 60 秒没被访问),避免 MySQL 主动断连后留下僵尸连接
- 禁止把连接实例序列化、存 Redis、跨进程传递——PDO 对象不可序列化
ORM 层怎么确保所有查询都走对的连接
不能依赖框架默认的 DB::table() 或 Model::find(),它们绑死在初始化连接上。必须显式传参或重写基类。
- 在模型基类里重写
newQuery(),从TenantContext::getContext()拿当前pdo实例,再 new 一个QueryBuilder - 或者统一走
Db::connection($pdo)->table('users')这种带实例的调用方式 - 如果用 Eloquent,需重写
getConnection()方法,返回当前租户的PDO封装对象 - 所有原始 SQL 执行(
$pdo->query())必须明确使用当前租户的$pdo,别混用
最容易被忽略的是连接关闭时机和协程上下文清理。很多线上数据错乱问题,不是租户识别错了,而是上一个请求的 tenant_id 残留在协程里,导致本该查 tenant_b 表的查询,实际执行在 tenant_a 的连接上——这种 bug 很难复现,但后果严重。

















