ThinkPHP 在 Swoole 协程环境下跑数据库,绝不是装个扩展、改个版本号就能搞定的事。问题的根源在于,ThinkPHP 默认的数据库连接机制(基于单例的 PDO 长连接)和 Swoole 的协程调度天生就“八字不合”。如果不做针对性调整,你会发现数据库连接要么被多个协程乱抢,要么直接报错,甚至引发事务状态污染。下面这几步,是必须踩实的硬功夫。

database.php 必改三项硬配置
很多同学以为装个 think-swoole 扩展就万事大吉了,其实最关键的配置藏在 database.php 里。不改这三项,协程数据库永远走不通:
- 'type' => 'mysql':强制使用 Swoole 原生协程 MySQL 客户端,禁用 pdo_mysql。为什么?因为 PDO 模式下的事务接口和协程对象不兼容,你用 PDO 等于还是同步阻塞,协程白搭。
- 'deploy' => 0:关闭读写分离。如果开启,
Connection类会绕过协程驱动,偷偷创建阻塞式连接,你的协程池就形同虚设了。 - 'params' => [PDO::ATTR_EMULATE_PREPARES => false]:禁用预处理模拟。否则 SQL 会被 PHP 同步拼接,失去协程化意义,性能反而更差。
另外有一点经常被忽略:'hostname'、'username'、'password' 必须明文写死,不能依赖 getenv() 或 $_ENV。因为协程上下文不保证环境变量始终可读,你线上调试时发现数据库连不上,多半就是这里出了问题。
禁止在初始化阶段执行数据库操作
协程还没启动就直接调 DB,你会看到一个经典的报错:SwooleCoroutineMySQL::query(): must be called in the coroutine。这个错误出现的位置往往很隐蔽——可能藏在模型的 __construct() 里,或者某个全局 Service Provider 的初始化方法中。
- 模型的
__construct()中绝对不要做查询或初始化 DB 实例。 - 全局 service 或 provider 初始化里避免
Db::table()->find()这类操作。 - 所有数据库调用必须发生在协程内,比如
go()函数、HTTP 请求回调、命令行handle方法中。
这个原则其实很简单:协程环境里的数据库操作,必须在协程上下文里执行,就像你进了游泳池才能游泳一样。
连接池不生效?检查调用方式是否正确
配置了连接池,但你发现 90% 的请求仍然走短连接?原因很简单:默认的 Db 门面类并没有走连接池逻辑。你得显式调用 Db::pool() 才能让连接池真正生效。
- 必须显式用
Db::pool()->table(...)->find(),不能省略::pool()。 - 事务要改写为
Db::pool()->transaction(function () { ... })。 - 模型中若需池化连接,要么重写
db()方法返回Db::pool(),要么构造时传入new UserModel(['db' => Db::pool()])。 - 读写分离场景下,
Db::connect('sla ve')是不进池的,必须用Db::pool()->connect('sla ve')。
可以理解为:连接池就像是一个共享单车池,你默认骑的是自己的私车,得主动去扫码解锁才能用池里的车。
服务提供者中接管连接工厂
think-swoole 扩展不会自动替换 Db::connect() 的行为,你必须手动“劫持”连接创建逻辑。最简单的做法是在 app/provider.php 或自定义服务提供者中绑定:
use thinkswoolepoolDatabasePool;
use thinkswoolepoolChannelPool;
Container::getInstance()->bind('db', function ($app) {
$config = $app->config->pull('database');
return new DatabasePool($config, new ChannelPool(64));
});
DatabasePool 会按 SwooleCoroutine::getuid() 分配隔离连接,确保每个协程独占连接,避免事务、fetch 模式等状态污染。这一步是很多教程里没讲透的,但恰恰是生产环境稳定运行的关键。