Laravel读写分离健康检查需在配置中显式设置PDO超时、用指定连接名执行SELECT 1并捕获异常、动态剔除宕机节点时须调用Config::set()和DB::purge(),且不可依赖reconnect()。

为什么 DB::connection()->getPdo() 会卡住不报错
读写分离环境下,Laravel 默认在首次执行查询时才真正建立从库连接。如果从库已宕机,DB::connection('read')->getPdo() 不会立即失败,而是阻塞在 TCP 握手或 DNS 解析阶段(取决于配置),默认超时可能长达数秒甚至更久——这会让健康检查变慢、拖垮整个探测流程。
- 必须显式设置 PDO 连接超时:
'options' => [PDO::ATTR_TIMEOUT => 2],加在config/database.php的从库配置里 - 不要依赖
getPdo()返回值判断连通性,它成功只代表“能拿到句柄”,不代表“能执行语句” - 真实健康检查必须发一条轻量 SQL,比如
SELECT 1,且要捕获PDOException和连接级错误(如SQLSTATE[HY000] [2002])
如何用 DB::select() 安全探测从库可用性
直接调用 DB::connection('read')->select('SELECT 1') 看似简单,但 Laravel 的读写分离策略会让它随机选一个从库;一旦某个从库宕机,这个调用就可能抛异常,而你未必知道是哪个节点出问题。
- 必须指定具体连接名:比如
DB::connection('mysql_read_1')->select('SELECT 1'),避免被负载均衡干扰 - 包裹
try/catch,捕获Illuminate\Database\QueryException和底层PDOException - 对返回结果不做业务逻辑假设,只要没异常 + 结果非空,就认为该从库当前可响应
- 示例片段:
try { $result = DB::connection('mysql_read_1')->select('SELECT 1'); return !empty($result); } catch (\Exception $e) { \Log::warning('Read node mysql_read_1 health check failed', ['error' => $e->getMessage()]); return false; }
自动剔除宕机从库后,如何避免“假恢复”误判
Laravel 没有内置的从库故障自动剔除机制。如果你自己实现定时探测+动态修改 config('database.connections.mysql.read'),要注意:配置是运行时缓存的,改了数组不会自动生效,下次请求仍可能打到刚被标记为“宕机”的节点。
- 不能只改配置数组,必须调用
Config::set()并重置连接实例:DB::purge('mysql_read_1') - 剔除不是永久删除,要记录最后失败时间,避免刚恢复就立刻被踢回黑名单;建议加 60 秒冷却窗口
- 别在每次请求里做健康检查——开销太大;应由独立的 Artisan 命令(如
php artisan db:health-check)定时跑,结果写入 Redis 或内存表供路由层读取 - 读写分离路由逻辑(
MyReadConnection类)需从外部数据源(而非硬编码配置)拉取当前可用从库列表
DB::connection()->reconnect() 在读写分离中根本不起作用
很多人以为调用 reconnect() 就能刷新从库连接,但实际上这个方法只对当前连接实例有效,而 Laravel 的读写分离是靠 ConnectionResolver 在查询前动态选节点的。你手动 reconnect 的那个实例,下一次查询时大概率已被丢弃,换成了另一个新实例。
-
reconnect()只适用于单连接场景,对read配置里的多个从库完全无效 - 真正要刷新的是连接池和路由映射,不是单个 PDO 对象
- 如果发现某从库恢复,不要等 Laravel 自动感知——必须触发一次主动重载:清空
DB::getConnectionResolver()缓存,并重新初始化所有 read 连接 - 最稳妥的做法是:服务端进程(如 Horizon worker)启动时加载可用节点列表,运行中只读不写;节点状态变更通过信号或共享存储同步,不依赖连接对象自身状态


















