ThinkPHP 8.0 在 Swoole 协程下需禁用 PDO、关闭读写分离、禁用预处理模拟,并使用 DatabasePool 替换 Db::connect(),显式调用 Db::pool() 才能实现协程安全的数据库操作。

ThinkPHP 8.0 在 Swoole 协程环境下不能直接用默认数据库配置跑起来——它会复用连接、卡死查询、事务报错、甚至 silently 丢数据。根本原因不是“没开协程”,而是 ThinkPHP 的连接管理机制和 Swoole 的协程调度模型天然冲突,必须显式切断旧路径、接管新流程。
Db::connect() 为什么在协程里返回同一个 PDO 实例
ThinkPHP 默认把 Db::connect() 设计成单例 + 连接复用,每次调用都返回同一个 PDO 对象。Swoole 协程共享 PHP 进程内存,多个协程拿到的其实是同一份连接状态。一旦协程 A 执行了 beginTransaction() 或 setFetchMode(PDO::FETCH_ASSOC),协程 B 下次查库就可能拿到错误的 fetch 模式,或事务还没提交就被意外 rollback。
- 这不是 bug,是同步模型下的合理设计,但完全不适用于协程
- 不能靠
new PDO()手动建连接——生命周期难管理,容易漏关、OOM - 必须让每个协程独占一个隔离连接,且由连接池自动分配和回收
database.php 必须改的三项硬配置
光装扩展没用,config/database.php 里这几项不改,协程数据库永远走不通:
-
'type' => 'mysql':强制走 Swoole 原生协程 MySQL 客户端,禁用pdo_mysql(PDO 模式下事务接口不兼容协程对象) -
'deploy' => 0:关闭读写分离,否则Connection类内部会绕过协程驱动,偷偷创建同步连接 -
'params' => [PDO::ATTR_EMULATE_PREPARES => false]:禁用预处理模拟,否则 SQL 会被 PHP 同步拼接,失去协程化意义
另外,'hostname'、'username'、'password' 必须明文写死,不能依赖 env() —— 协程上下文不保证环境变量始终可读。
立即学习“PHP免费学习笔记(深入)”;
think-swoole 的 DatabasePool 怎么真正生效
think-swoole 自带连接池,但它不会自动替换 Db::connect() 行为。你得手动“劫持”连接创建逻辑:
- 在
app/provider.php或自定义服务提供者中绑定:use think\swoole\pool\DatabasePool;<br>use think\swoole\pool\ChannelPool;<br>Container::getInstance()->bind('db', function ($app) {<br> $config = $app->config->pull('database');<br> return new DatabasePool($config, new ChannelPool(64));<br>}); -
DatabasePool会拦截所有Db::connect()调用,并按Swoole\Coroutine::getuid()分配连接 - 池大小建议设为
max_request * 2(如 Swoole 配置max_request => 3000,池大小设 60–100),太大易 OOM,太小导致频繁创建销毁 - 关键提醒:
Db::table()->select()仍走旧路径!必须显式写Db::pool()->table()->select()才进池
事务、Session、Redis 全链路协程对齐要点
只改数据库不够,其他组件不同步照样拖垮整个协程流:
-
Db::transaction()不能直接用——它底层调的是mysqli_commit()这类函数,而协程 MySQL 返回的是对象。改用Db::pool()->transaction() - Session 必须禁用
session.auto_start,改用手动控制:session_id($id); session_start();,并在响应结束前调session_write_close() - Session 存储必须切到 Redis,且开启
session.use_strict_mode = 1,避免协程间 session_id 冲突 - Redis 客户端也要换:用
swoole_redis或co-redis,别用phpredis(同步阻塞)
最容易被忽略的是:Swoole 的 SWOOLE_HOOK_PDO 或 SWOOLE_HOOK_ALL 必须在 WorkerStart 回调里设置,不能在全局提前设——框架初始化阶段可能覆盖掉它。



















