thinkphp读写分离需显式配置'deploy'=>1、'rw_separate'=>true及非空'read'数组,否则select始终走主库;db::master()仅对当次链式调用生效,事务中所有查询强制走主库,getlastsql()不反映实际执行库。

ThinkPHP 的读写分离配置不是“设了就生效”,而是极易因连接池复用、查询类型识别偏差或事务干扰导致从库查询被跳过——最常见现象是 SELECT 语句始终走主库。
为什么 $db->query() 强制走主库
ThinkPHP 在执行原生 SQL 查询时,会根据 SQL 关键字做简单判断:SELECT 走从库,其余走主库。但这个判断发生在解析 SQL 字符串阶段,不经过语法树分析。
以下情况会导致误判:
-
SELECT前带注释(如/*+ USE_INDEX(users,PRIMARY) */ SELECT * FROM users)→ 匹配失败,降级走主库 - SQL 中含子查询或 CTE(如
WITH tmp AS (SELECT 1) SELECT * FROM tmp)→ 首词不是SELECT,走主库 - 使用了大写混合或空格变形(如
sELECT、SELECT\nFROM)→ 正则匹配不敏感,直接失效
解决方案:显式指定连接类型,绕过自动识别逻辑:
$db->master()->query('SELECT * FROM users WHERE id = ?', [$id]);
// 或
$db->slave()->query('SELECT * FROM users');
事务中连 SELECT 都不走从库
只要当前请求处于事务上下文(哪怕只是 Db::startTrans() 后还没提交),所有查询——包括 SELECT ——都会强制路由到主库。这是为保证事务内读写一致性,但也是最容易被忽略的“静默失效”点。
典型陷阱场景:
- 控制器里开了事务,中间调用了某个 service 方法,该方法内部做了
User::where(...)->find() - 模型事件(如
after_read)里触发了额外查询,此时仍在事务中 - 全局中间件开启了事务(如日志记录中间件误启事务)
验证方式:开启数据库慢日志或抓包,看实际连接的 IP 是否为主库地址;不要只信日志里的“SQL: SELECT…”。
Db::table() 和模型查询的读写行为差异
两者底层都走 Query 类,但初始化连接策略不同:
-
Db::table('users')->select():默认启用读写分离,按规则选从库(前提是没进事务、没用原生 SQL 且 SQL 可识别) -
User::select():若模型定义了protected $connection = 'my_config',则完全绕过全局读写配置,走该连接名下的主库配置 - 更隐蔽的是
with()预加载:即使主查询走从库,关联查询仍可能走主库,因为with内部新建了独立 Query 实例,未继承主 Query 的从库上下文
建议统一用 Db 查询做读操作,模型仅用于写或强约束场景;若必须用模型读,加 ->useReadConnection(true) 显式声明(TP6.3+ 支持)。
从库延迟导致数据不一致却无报错
ThinkPHP 不检测从库同步延迟,也不提供延迟阈值熔断机制。当主库刚插入一条记录,紧接着在另一个请求里用 Db::table('orders')->where('user_id', $uid)->select() 查询,很可能查不到——因为从库还没追上。
这不是框架 bug,而是架构取舍。应对方式只有两种:
- 对强一致性读(如刚下单查订单详情),主动切回主库:
Db::master()->table('orders')->where(...)->select() - 业务层加重试或缓存兜底,避免用户看到“数据消失”
别依赖 lastInsertId() 后立刻查,那条记录在从库上大概率还不可见。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











