hyperf 3.0 中“数据库连接被抢占”本质是协程间误用静态变量共享请求级数据,导致连接、事务、用户身份等上下文串扰;应改用 context::set/get 隔离协程状态,并合理配置连接池与显式释放资源。

Hyperf 3.0 中数据库连接被“抢占”,本质不是连接真被别的协程拿走,而是协程间共享了不该共享的状态,最典型的就是误用静态变量或类属性存连接、事务上下文、用户身份等请求级数据。协程切换本身不会偷连接,但错误的数据存放方式会让后续协程读到前一个请求残留的连接句柄,从而查错库、写错表、越权操作。
为什么静态变量会导致“连接被抢占”
Hyperf 运行在 Swoole 协程环境,多个请求共用同一个 Worker 进程,每个请求在一个独立协程中执行。而 static $conn 或 static $user_id 是进程级全局变量——A 请求写入后没清理,B 请求进来直接读取,就出现“连接串流”:
- MySQL 查询返回其他用户的订单数据
- 事务 commit 时提示 “No active transaction”(实际是上个协程的事务没关)
- Redis 写入
set('token', 'abc'),下个协程get('token')拿到的是旧值
正确做法:用 Context 隔离协程上下文
Hyperf 提供 Context::set() 和 Context::get(),底层绑定到当前协程 ID,确保数据严格隔离:
-
Context::set('db_conn', $conn)只对当前协程生效,子协程不继承(除非显式Context::copy()) - 协程结束时,对应上下文自动释放,无需手动清理
- 中间件、Repository、DAO 层中所有与单次请求强绑定的数据(如 auth info、租户 ID、临时连接)都应进 Context,而不是类属性或 static
连接池配置要匹配真实并发压力
连接“不够用”会被误认为“被抢占”,其实是连接池耗尽导致新协程卡在 wait_timeout 上:
- 检查
config/autoload/databases.php中pool.max_connections是否足够:建议设为峰值 QPS × 平均查询耗时 × 2~3 倍余量(例如 80 QPS × 0.15s = 12,可设为 40) -
pool.wait_timeout推荐设为2.0–5.0,太小会频繁报WaitTimeoutException,太大掩盖瓶颈 - 用
swoole_get_local_socket_count()或ss -s | grep ESTAB观察实际连接数是否长期逼近max_connections
避免事务/管道未释放引发连接泄漏
调用 $redis->pipeline() 或 Db::transaction() 后若中途异常退出,连接可能被独占不归还:
- 错误写法:
try { $db->transaction(...); throw new Exception(); }→ 连接未释放 - 正确写法:用
try/finally显式commit/rollback,或改用 Hyperf 3.1+ 提供的withTransaction()封装 - 验证方式:在关键路径加日志,检查
Context::has($db->getContextKey())是否为 true 但未完成事务











