
php 本身没有内置连接池
PHP 是无状态的脚本语言,每次请求结束后所有资源(包括数据库连接)都会被销毁。所谓“PHP 连接池”,其实是外部组件或中间件实现的,不是 mysqli 或 PDO 自带的功能。
常见误解是以为调用 new PDO() 就能复用连接——实际每次 new 都新建 TCP 连接(除非启用了 MySQL 的 mysqlnd 持久连接,但那不是连接池,只是连接缓存,且有严重线程安全风险)。
- 持久连接(
PDO::ATTR_PERSISTENT => true)只在 CLI 或 Apache prefork 模式下“看似有效”,在 FPM 下几乎无效,且容易导致连接数爆炸 - 真正意义上的连接池必须由独立进程/服务管理连接生命周期,比如
ProxySQL、MySQL Router、或 Swoole 的Swoole\Coroutine\MySQL协程池 - 如果你用的是 Laravel + Octane / Swoole,注意
DB::connection()默认仍走传统阻塞连接,需显式切换到协程 MySQL 客户端
用 Swoole 实现协程 MySQL 连接池最实用
这是目前 PHP 生态里最接近“开箱即用连接池”的方案:连接由协程调度器统一维护,多个协程可复用同一底层连接,避免频繁建连/断连开销。
关键点不在“池子有多大”,而在“怎么确保不超限、不泄漏、不混用”:
立即学习“PHP免费学习笔记(深入)”;
- 必须设置
max_idle_time和max_active,否则空闲连接一直占着,DB 侧会报Too many connections - 不能在协程外调用
$pool->get(),否则会阻塞整个进程;务必在go或Co\run内使用 -
Swoole\Coroutine\MySQL不兼容原生PDO语法,fetch()返回的是数组而非对象,prepare也不支持命名参数(只能用?) - 示例片段:
$pool = new \Swoole\Coroutine\Pool(10, 0.1, 60); $pool->set([ 'host' => '127.0.0.1', 'port' => 3306, 'user' => 'root', 'password' => '', 'database' => 'test', 'max_idle_time' => 30, ]); $pool->on('create', fn() => new \Swoole\Coroutine\MySQL());
ProxySQL 是最稳妥的跨语言连接池方案
如果你的应用不止有 PHP,还有 Python、Node.js 等服务共用同一套 MySQL,直接在中间加一层 ProxySQL 是更健壮的选择——它把连接池逻辑从应用层彻底剥离。
它不改代码,只改连接地址,但要注意几个硬性配置项:
- 必须开启
mysql-native_password插件支持(新版 MySQL 默认用caching_sha2_password,ProxySQL 旧版本不认) -
mysql-pool-mode要设为transaction,否则事务中连接可能被其他协程抢走 - 监控
stats_mysql_connection_pool表,重点看ConnUsed和ConnFree,持续为 0 说明没生效,可能是应用连错了端口(该连6033而非3306) - 错误信息如
Access denied for user 'xxx'@'proxy' using password: YES,大概率是 ProxySQL 用户没在mysql_users表里配好
FPM 场景下别硬凑连接池
PHP-FPM 是多进程模型,每个 worker 进程独立内存空间,没法共享连接句柄。此时强行做“池”,只会让每个进程都维持一套连接,反而加重 DB 压力。
真正该优化的是连接建立和释放环节:
- 确认 MySQL 服务端
wait_timeout和interactive_timeout设置合理(建议 ≥ 300),避免 PHP 还没用完连接就被 DB 主动断掉 - 用
mysqli_options($link, MYSQLI_OPT_CONNECT_TIMEOUT, 1)控制建连超时,防止一个慢连接拖垮整个请求 - 如果用 PDO,禁用
PDO::ATTR_EMULATE_PREPARES(设为false),否则预处理语句会绕过 MySQL 的 prepare 缓存,失去服务端执行计划复用优势 - 不要在循环里反复
new PDO,哪怕只是查一条记录——把连接作为依赖注入或单例管理,比任何“池”都实在











