hyperf 3.1 协程 mysql 连接污染本质是连接被错误复用或状态未隔离,根源在于多个协程共享同一连接实例(如静态变量、手动 pdo、非池化 mysql),导致事务未提交、库切换错乱、server has gone away 等问题;正确做法是必须通过 di 获取 db 实例、走连接池、启用 auto_ping、事务用 db::transaction() 包裹,并用 context 隔离业务数据而非塞入连接对象。

Hyperf 3.1 中协程 MySQL 客户端的连接污染,本质不是“协程切换导致”,而是**连接被错误复用或状态未隔离**——多个协程共用一个 MySQL 连接实例(比如存在静态变量、全局单例、手动管理的 PDO 实例),当协程 A 拿到连接执行完查询后未归还或未重置状态,协程 B 再次拿到它时,可能遭遇连接已断、事务未提交、数据库上下文(如 SELECT DATABASE())错乱等问题。
根本原因:连接没做到“一协程一连接”
常见错误模式:
- 在类中用
static $mysql或private $mysql缓存连接 —— 单例对象跨协程共享,状态互相覆盖; - 手动 new Swoole\Coroutine\MySQL() 后长期持有,不走连接池,也不做 ping 校验;
- 在事务中混用 DB::transaction() 和原生 $mysql->query(),导致底层连接状态与 ORM 不一致;
- 使用了非协程安全的驱动(如普通 PDO),其连接句柄被多协程并发读写,触发
MySQL server has gone away。
正确做法:靠连接池 + 上下文绑定 + 每次校验
Hyperf 默认的 Hyperf\DbConnection\Db 已内置协程安全机制,关键在于用对、配对、不绕过:
- ✅ 必须通过 DI 容器获取 DB 实例(如
$this->db或DB::connection()),不要 new; - ✅ 所有数据库操作必须走连接池:配置
databases.default.pool.max_connections合理值(建议 50–100),并启用max_idle_time: 30自动清理空闲连接; - ✅ 每次 query 前自动 ping(Hyperf 3.1 默认已开启
auto_ping: true,检查配置确保未关闭); - ✅ 事务必须用
DB::transaction()包裹,避免手动 begin/commit,防止跨协程污染连接事务状态; - ✅ 若需在子协程中复用父协程的数据库上下文(如租户库名、读写分离标记),应显式传参或通过
Context::set('tenant_db', 'db_x')+ 自定义中间件注入,而非依赖连接对象自身属性。
进阶隔离:用户级/请求级数据不污染连接
连接本身要干净,但业务数据(如当前用户 ID、租户标识)不能塞进连接对象里。否则又回到静态变量污染的老路:
- ❌ 错误:在
Connection子类里加public $userId并赋值; - ✅ 正确:用
Context::set('user_id', $uid)存储请求身份,需要时在查询前从 Context 取出,拼入 SQL 或传给 QueryBuilder; - ✅ 更规范:封装成自定义
TenantConnection,在selectDatabase()前从 Context 读取租户信息,动态切换库,且该逻辑只在当前协程生效。
验证是否真隔离:简单自测法
写一个并发压测脚本,启动两个协程:
- 协程 A:执行
SELECT SLEEP(1); SELECT USER();(模拟长耗时+查用户); - 协程 B:紧随其后执行
SELECT DATABASE();; - 若 B 返回的是 A 切换后的库名,或出现
SQLSTATE[HY000]: General error: 2006 MySQL server has gone away,说明连接未隔离或未校验; - 若两次查询各自返回预期结果、无串库无报错,说明连接池和 auto_ping 工作正常。











