hyperf 中 db 查询必须在协程上下文执行,否则报“must be called in the coroutine”错误;控制器方法默认协程安全,命令行需用 go 或 abstractcommand;避免在 __construct/static 中查询;db:: 与 eloquent 连接复用行为不同,跨模型事务应统一连接或显式使用 db::transaction();并发查询须用 co::wait 或 channel 控制,防止连接池耗尽。

Hyperf 里用 Db 查询必须在协程上下文里执行
Hyperf 的数据库组件(如 hyperf/database)底层依赖 Swoole 协程 MySQL 客户端,所有查询操作天然要求运行在协程中。如果你在非协程环境(比如 __construct、static 方法、或被 go 包裹之外的普通方法)里调用 Db::select() 或 UserModel::find(),会直接报错:Swoole\Coroutine\MySQL::query(): must be called in the coroutine。
常见错误场景包括:在控制器构造函数里查库、在命令行 Command 类的 handle() 外部初始化模型、或把查询逻辑写在 static 工具方法里却没确保调用方已进入协程。
- 控制器里查库,确保方法本身被框架自动调度进协程(默认 Controller 方法都是协程安全的)
- 命令行任务需显式用
go包裹,或继承AbstractCommand并在handle()中写逻辑(该方法已被框架自动协程化) - 避免在
__construct或static方法里触发查询;改用 lazy 初始化或传参方式延迟执行
Db:: 和 Eloquent 模型在协程里写法一致但行为有差异
两者都支持协程,但连接复用和事务行为不同。Db:: 是原生查询构建器,每次调用默认使用连接池中的空闲连接;Eloquent 模型则可能隐式复用同一个连接(尤其在事务块内),这点容易被忽略。
例如开启事务后,Db::transaction() 和 DB::transaction()(Laravel 风格别名)本质相同,但若混用 Db:: 和 Model::,且未显式指定连接名,它们可能命中不同连接,导致事务不生效。
- 统一用
Db::做简单查询,用Model做带关系、访问器、事件的业务逻辑 - 跨模型操作需事务时,优先用
Db::transaction()显式包裹,或确保所有模型操作都在同一连接上(通过on('default')或配置pool.enable_transaction) - 注意
Model::query()->lockForUpdate()在协程下有效,但锁等待超时由mysql.connect_timeout和wait_timeout共同影响,不是纯协程超时控制
协程并发查库别直接 go + foreach,要用 Co::wait 或 Channel
想并发查 5 张表,写成 foreach ($ids as $id) { go(fn() => Db::table('user')->where('id', $id)->first()); } 看似省事,实际无法收集结果,还可能因协程数量失控拖垮连接池。
正确做法是用 Co::wait() 等待一组协程完成,并收集返回值;或者用 Channel 做结果聚合,更可控。
// 推荐:用 Co::wait 收集结果
$promises = [];
foreach ([1, 2, 3] as $id) {
$promises[] = go(function () use ($id) {
return Db::table('user')->where('id', $id)->first();
});
}
$results = array_map(fn($p) => $p->get(), $promises);
- 不要无限制
go,连接池大小默认 10,超出会阻塞等待;可按需设置pool.min_connections和pool.max_connections -
Db::查询默认不启用 prepared statement,高频小查询建议开启options' => [PDO::ATTR_EMULATE_PREPARES => false]提升性能 - 如果某次查询耗时异常长(如 >1s),检查是否触发了慢日志、索引缺失,协程不会帮你优化 SQL
调试协程查库问题,重点看 coroutine_id 和连接池状态
当出现“查询卡住”“返回空数组但无报错”“偶发连接超时”,不要只盯 SQL 日志。Hyperf 的协程数据库操作生命周期和普通 PHP 完全不同,得从协程 ID 和连接池水位切入。
可在查询前后打点:var_dump(co::tid()) 确认是否仍在同一协程;用 Db::getConnection()->getPool()->getStats() 查当前活跃连接数、等待请求数。如果 waiting 持续 >0,说明连接池不够或有长连接未释放(比如忘了 finally 关闭事务)。
- 事务务必配对:有
beginTransaction()就要有commit()或rollback(),否则连接会被长期占用 - 模型中慎用
$model->load('relation'),N+1 问题在协程里放大更快;改用with('relation')预加载 - Hyperf v3.1+ 默认启用连接池健康检测,但若 MySQL 主从延迟大,
ping可能误判,可临时关闭pool.health_check排查
Db:: 调用,得顺手想清楚它拿的是哪个连接、会不会阻塞别人、失败后连接还回池子了吗。











