ThinkPHP 默认无连接池,Db::connect() 每次新建 PDO 实例,Swoole 等常驻进程下易触发 MySQL 连接数超限;需手动实现轻量静态连接池,配合 persistent 配置、键值隔离、峰值监控与计数管理。

ThinkPHP 连接池不是内置功能,Db::connect() 默认不复用连接
ThinkPHP 6/7 的 Db::connect() 每次调用都新建 PDO 实例(除非显式传入已存在的连接),底层没有连接池管理逻辑。所谓“连接池”是开发者自己封装的,框架只提供连接抽象,不负责生命周期复用。
这意味着:直接写 Db::connect('mysql2') 十次,就可能打开十个 TCP 连接——尤其在协程环境(如 Swoole)或长生命周期 Worker 中,极易触发 MySQL 的 max_connections 限制。
- 真实场景:Swoole HTTP Server 启动 20 个 Worker,每个 Worker 在处理请求时反复调用
Db::connect(),未释放或复用,1 分钟内打满 MySQL 的 100 连接上限 - 错误现象:
SQLSTATE[HY000] [1040] Too many connections频繁报出,但show processlist显示大量 Sleep 状态连接未及时关闭 - 关键原因:ThinkPHP 默认使用 PHP-FPM 模式设计,依赖进程退出自动销毁连接;Swoole/Workerman 下进程常驻,PDO 对象不被析构 → 连接不释放
手动实现轻量连接池:用静态容器 + 连接计数器
不需要引入第三方池组件,用一个带计数和超时淘汰的静态数组就能解决大部分预警需求。重点不是“池有多智能”,而是“能看见谁占着不放”。
示例结构(放在 app/common/DbPool.php):
class DbPool
{
protected static $pool = [];
protected static $counter = [];
<pre class="brush:php;toolbar:false;">public static function get($configName)
{
$key = md5(serialize(config("database.connections.{$configName}")));
if (!isset(static::$pool[$key]) || static::$pool[$key] === null) {
static::$pool[$key] = Db::connect($configName);
static::$counter[$key] = 0;
}
static::$counter[$key]++;
return static::$pool[$key];
}
public static function release($configName)
{
$key = md5(serialize(config("database.connections.{$configName}")));
if (isset(static::$counter[$key]) && static::$counter[$key] > 0) {
static::$counter[$key]--;
}
}
public static function stats()
{
return static::$counter;
}
}
- 每次
DbPool::get('mysql')返回同一个连接实例,release()只减计数,不真正 close —— 因为 PDO 不支持 close 后 reopen,且多数场景下保持连接比反复握手更稳 - 必须配
pdo->setAttribute(PDO::ATTR_PERSISTENT, true)(在数据库配置里加'persistent' => true),否则即使复用对象,底层仍是新连接 - 注意:该方案不适用于读写分离场景下需动态切换主从连接的情况;若需分主从,键名应包含 role 标识,例如
"mysql_master"/"mysql_slave_1"
监控峰值:在 DbPool::get() 里埋点统计并发连接数
真正的瓶颈不在“平均连接数”,而在“瞬时并发峰值”。不要等报错才响应,要在连接被取走的那一刻记录当前计数。
改写 get() 方法加入原子计数与阈值判断:
public static function get($configName)
{
$key = md5(serialize(config("database.connections.{$configName}")));
if (!isset(static::$pool[$key])) {
static::$pool[$key] = Db::connect($configName);
static::$counter[$key] = 0;
}
<pre class="brush:php;toolbar:false;">static::$counter[$key]++;
$current = static::$counter[$key];
// 峰值预警:单配置连接数超 30 就记录日志(按实际 max_connections * 0.7 设)
if ($current > 30) {
\think\Log::warning("DB POOL PEAK ALERT: {$configName} reached {$current} active uses", [
'pool_key' => $key,
'time' => date('Y-m-d H:i:s'),
'pid' => getmypid(),
]);
}
return static::$pool[$key];
}
- 别依赖外部监控工具先采集再报警——延迟高、漏报多;连接池内部埋点最准,且可结合
getmypid()定位到具体 Worker - 阈值设为 MySQL
max_connections的 60%~70%,留出系统账号、备份、DBA 临时连接的空间 - 避免在
release()里记日志:释放是常态,不反映压力;只有“获取”动作才代表资源争抢发生
为什么不用 Db::close() 或 unset($db)?
ThinkPHP 的 Db::close() 实际调用的是 PDO 的 null 赋值,不触发 __destruct,也不关闭底层 socket。而 unset() 在常驻进程中根本不会触发析构——PHP 的 GC 不保证立即回收。
- 现象:
Db::close('mysql')后再Db::connect('mysql'),show processlist仍显示旧连接为 Sleep 状态,新连接又新增一条 - 根本问题:PDO 对象被 unset 后,只要还有变量引用(比如你存到了静态数组、闭包、全局数组里),就不会销毁;而 ThinkPHP 的连接管理器默认会缓存连接实例
- 正确做法:放弃“手动关连接”的执念,转为“控制连接来源唯一性 + 监控取用频次”。连接池的意义是减少创建,不是模拟短连接
复杂点在于:不同部署模型(FPM/Swoole/Workerman)对连接生命周期的理解完全不同,但 ThinkPHP 的 Db 类没做区分。所以最稳的路径不是修框架,而是绕过它默认行为,自己管住“谁在什么时候拿了哪个连接”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










