在thinkphp中需监听db::listen事件,用正则匹配sql关键字判断读写类型,记录耗时与连接配置,禁用预处理查主从延迟,并通过版本号或微延迟策略应对“假延迟”。

如何在ThinkPHP中拦截并记录SQL执行的读写类型
ThinkPHP本身不区分读写SQL,得靠手动识别关键字来判断。核心思路是监听Db::listen事件,在回调里用正则匹配SELECT、INSERT等关键词——但注意大小写和空格干扰,比如select可能被写成SELECT或 select 。
实操建议:
- 用
preg_match('/^\s*(SELECT|SHOW|EXPLAIN|DESCRIBE)/i', $sql)判断是否为读操作,比stripos()更可靠,避免误判INSERT INTO ... SELECT ...这类混合语句 - 写操作统一归为非读,但要单独排除
REPLACE(它本质是写,但语法像INSERT) - 别依赖
$query->getOptions()['master'],这个值只在显式调用master()时才存在,日常Db::table()->select()根本不会设它 - 监控日志里必须带上
$info['time'](毫秒级耗时)和$info['type'](read/write),否则无法关联分析主从延迟
为什么Db::getLastSql()不能用于实时读写分离判断
因为Db::getLastSql()只返回最后一条SQL,而一个请求里可能有多个查询、多次切换连接(比如先查缓存再查库),它既不反映执行时实际走的连接,也不包含执行上下文。
常见错误现象:
- 在事务里调用
Db::getLastSql(),拿到的是START TRANSACTION而不是后续的UPDATE语句 - 用
Db::connect('slave')手动切从库后,getLastSql()仍可能返回主库的SQL(因底层连接池未刷新) - 开启查询缓存时,SQL可能根本没执行,
getLastSql()却返回了缓存前的语句
真正可用的方式是改写think\db\Connection类的query和execute方法,或在Db::listen回调的$info参数里取$info['connection']->getConfig()['hostname']——这才是实际执行时连接的真实配置。
主从延迟检测必须绕过PDO预处理缓存
ThinkPHP默认开启PDO预处理('deploy' => 1时自动启用),而预处理语句会复用执行计划,导致SHOW SLAVE STATUS这类管理命令被缓存或拒绝执行。
正确做法:
- 检测延迟时,必须用独立连接(
Db::connect(['hostname' => 'slave_ip', 'deploy' => 0])),禁用预处理 - SQL必须用
SELECT UNIX_TIMESTAMP() - UNIX_TIMESTAMP(Seconds_Behind_Master)这种写法,不能直接查Seconds_Behind_Master字段,因为MySQL 5.7+对只读账户隐藏该字段,需配合SELECT权限绕过 - 延迟值为
NULL通常代表IO线程断开,不是0;为0才表示实时同步,但要连续三次检测都为0才算稳定 - 别在Web请求中实时查延迟,应由定时任务(如Linux cron)写入Redis,接口只读缓存,否则高并发下从库自身压力会加剧延迟
从库“假延迟”:为什么SHOW SLAVE STATUS显示正常但业务读到旧数据
这是最易被忽略的问题——MySQL报告Seconds_Behind_Master = 0,但应用仍读到过期数据。根本原因是GTID模式下,从库SQL线程已追平,但事务尚未提交(commit未刷盘),或者主库开启了binlog_order_commits=OFF,导致从库回放顺序与主库不一致。
应对方式:
- 在关键读操作前加
SELECT MASTER_POS_WAIT(..., 0.1)(需提前获取主库位点),但仅限强一致性场景,会明显拖慢响应 - 更实用的是用
SELECT SLEEP(0.01)微延迟 + 再查一次,适用于订单详情页这类对延迟容忍度低但又不能锁表的场景 - 终极方案是业务层加版本号或时间戳字段(如
updated_at),读取后对比当前时间,偏差超阈值(如>500ms)则自动fallback到主库重查
复杂点在于:这些策略都要和ThinkPHP的查询构造器解耦,不能塞进where条件里,得在Db::listen回调中根据SQL内容动态注入,否则会污染业务逻辑。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











