ThinkPHP 6.x读写分离需同时满足deploy=1、rw_separate=true,read必须为二维数组,database大小写须与write一致;事务、锁、特定连贯操作会强制回落主库;从库故障需手动try/catch兜底。

主从切换异常不是配置没写对,而是读写分离开关没开、从库结构配错、或事务/锁导致自动回落主库——这三处不检查,调再多次 Db::connect() 都没用。
为什么 read 配置写了却始终走主库?
ThinkPHP 6.x 不会因存在 read 数组就自动启用读写分离。必须同时满足两个硬性条件:
-
connections['mysql']配置块内显式声明'deploy' => 1和'rw_separate' => true;漏掉任一,整个读写分离逻辑被跳过 -
read必须是「数组套数组」,哪怕只配一个从库也要写成'read' => [[...]],写成'read' => [...]会导致框架解析失败,后续调用直接报Call to a member function query() on null - 所有
read节点的database字段值必须和write完全一致(包括大小写),否则 JOIN 或跨表查询会出错,且部分查询会静默 fallback 到主库
select() 还是走了主库?检查有没有触发“强制回落”条件
ThinkPHP 的读写路由不是按 SQL 类型判断,而是按方法语义 + 上下文状态。以下任意一种情况都会让本该走从库的 select() 自动切回主库:
- 当前连接已开启事务(
startTrans()之后的所有操作) - 查询链中调用了
lock(true)或lock('for update') - 连贯操作含
order()+limit()+count()组合,框架可能误判为写意图 - 手动调用了
$db->master()->select(),但没在后续链中显式延续该连接实例
验证方式:在查询前加 dump(Db::getLastSql()),再查 MySQL 的 show processlist,看实际连接的 host 是主库还是从库 IP。
立即学习“PHP免费学习笔记(深入)”;
从库故障时如何不崩、不卡、自动切主?
ThinkPHP 原生不提供从库健康检测和自动兜底。它只会随机选一个 read 节点发起连接,失败就抛异常,不会尝试下一个或切主库。
- 捕获异常必须用
try/catch包住整个查询,不能只包select()前半段 - 推荐写法:
try { $data = Db::table('user')->select(); } catch (\PDOException $e) { if (strpos($e->getMessage(), 'Connection refused') !== false) { $data = Db::master()->table('user')->select(); } } - 不要在循环里反复
Db::connect('slave_x'),复用同一个连接对象;更别在模型initialize()中动态改$this->connection,它不响应运行时变化 - 若需轮询而非随机,必须重写
think\db\Connection::getReadConnection()方法,用静态计数器控制下标,并在异常时递增跳过
多租户场景下,Db::connect('tenant_123') 为何不走读写分离?
因为 Db::connect('xxx') 创建的是完全独立的连接实例,它只认自己配置里的 hostname,无视全局 read/write 结构。自定义连接名 = 主动放弃读写分离能力。
- 正确做法:所有租户共用
connections['mysql'],通过中间件动态修改其read数组内容(例如只保留该租户专属的从库列表) - 改完后必须清空
Db::$connections缓存,否则下次请求仍用旧配置:Db::clearConnections() - 改配置后,所有后续
Db::table()查询才生效;Db::connect('xxx')的调用依然无效
最易忽略的一点:Db::setConnectConfig() 注册新连接后,必须在任何数据库操作之前完成绑定,否则第一次 Db::table() 已初始化默认连接,后面怎么注册都晚了。



















