webman可通过协程安全上下文、可控中间件顺序和动态数据库管理实现生产级多租户;需在首个中间件基于http_host识别租户并调用tenantcontext::setcontext(),严格隔离连接、缓存、日志及查询上下文。

Webman 本身不提供开箱即用的多租户能力,但它的协程安全上下文、中间件执行顺序可控、数据库连接可动态管理这三点,足以支撑生产级多租户系统。关键不是“有没有内置功能”,而是你能否在请求最早期就锁死 tenant_id,并确保后续所有数据操作(查询、写入、缓存、日志)都自动携带这个上下文。
租户识别必须在第一个中间件完成
识别晚了,模型构造、日志记录、缓存写入可能已发生,导致跨租户污染。不能依赖 session、URL 参数或 request()->domain() —— CLI、队列、WebSocket 场景下这些都不可用或被污染。
- 优先从
$_SERVER['HTTP_HOST']或反向代理下的$_SERVER['HTTP_X_FORWARDED_HOST']提取原始域名(如tenant1.example.com) - 查库匹配租户记录时,避免先查租户表再连库——容易形成死循环;建议用域名映射规则生成库名(如
tenant1.example.com→db_tenant1) - 识别后立刻调用
TenantContext::setContext(),把tenant_id、db_name、isolation_mode写入当前协程上下文 - 该中间件必须注册在
app/middleware.php的最顶部,且不能被其他中间件提前终止
数据库连接不能复用全局配置
Webman 默认 PDO 连接池是单例+懒加载,改 config 不会重建连接。现象是:明明设了新库名,Db::table('users')->get() 还是从旧库查。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 独立数据库模式:在
TenantContext::setContext()后,用原生PDO新建连接,并绑定到容器特定 key(如db.tenant),所有租户内模型强制使用该连接实例 - 共享库 + 独立 Schema 模式:修改
database.php中的dbname为占位符,在模型基类getTableName()中拼接前缀:return $this->prefix . TenantContext::id() . '.' . $this->table; - 务必显式调用
Db::clearConnection('tenant')清空旧连接缓存,否则连接复用会导致错库 - 禁止用
Db::table()绕过模型——它不走模型的连接绑定逻辑,也不触发租户前缀拼接
行级隔离不能只靠 tenant_id 字段
即使表里有 tenant_id 列,User::find(1) 仍可能查出其他租户用户。Eloquent/ThinkPHP/Webman ORM 都不自动加租户条件,这是设计使然,不是 bug。
- 不要依赖“全局 scope”自动注入
WHERE tenant_id = ?——它对Db::table()、原生 SQL、withTrashed()完全无效 - RBAC 必须分层:平台层校验租户是否启用;租户层校验接口权限;数据层强制通过模型重写
baseQuery()或使用where('tenant_id', TenantContext::id())做兜底 - 缓存键必须包含
TenantContext::id(),否则cache()->get('user_list')可能返回其他租户数据 - 日志记录需在
Log::channel('tenant')中显式注入上下文,防止日志混杂
最容易被忽略的是协程上下文清理和连接缓存失效——TenantContext::destroy() 必须放在中间件末尾的 try/finally 块中执行,否则协程复用时残留上一个租户的 tenant_id,这是线上最隐蔽的数据越界来源。










