如何配置ThinkPHP6的数据库读写分离

文章导读
有朋友问 ThinkPHP6 怎么配置读写分离,我先不急着给配置,先聊聊我会先确认哪些东西。读写分离并不适合所有系统,先确认业务是否存在明显的读多写少特征。如果大部分请求都是查询,且对数据实时性要求不高,例如文章列表、商品展示这类场景,才值得引入。判断时还要留意从库延迟:在同一事务或一次用户请求内完成了写库又立刻读库,此时数据必须是最新的,这种请求不能走从库,否则会出现刚提交的内容读不到的情况。如
📋 目录
  1. A 配置连接:read 与 write 数组
  2. B 强制主库的查询场景
  3. C 容易忽略的坑:分页、事务和模型查询
  4. D 验证配置是否生效
  5. E 风险边界与回滚思路
A A

有朋友问 ThinkPHP6 怎么配置读写分离,我先不急着给配置,先聊聊我会先确认哪些东西。读写分离并不适合所有系统,先确认业务是否存在明显的读多写少特征。如果大部分请求都是查询,且对数据实时性要求不高,例如文章列表、商品展示这类场景,才值得引入。判断时还要留意从库延迟:在同一事务或一次用户请求内完成了写库又立刻读库,此时数据必须是最新的,这种请求不能走从库,否则会出现刚提交的内容读不到的情况。如果业务中这类实时读的占比很高,读写分离的价值就会大打折扣。

配置连接:read 与 write 数组

如果确认业务确实适合读写分离,那么配置本身并不复杂。ThinkPHP6 的读写分离不需要在代码里手动切换连接,只需修改 config/database.php 的数据库配置。可以给连接参数增加 read 和 write 两个数组,分别指定主库和从库的 host、用户名、密码等。例如 write 里填主库地址,read 里填一个或多个从库地址,数据库类型、数据库名等公共参数放在外层。配置完成后,thinkphp 会按照默认规则自动分配:写操作走 write 中的连接,查询操作会在 read 数组中随机选择一台从库执行。

下面是一个最小化的配置示例:

'mysql' => [
    'type'     => 'mysql',
    'host'     => '主库IP',
    'database' => 'dbname',
    'username' => 'user',
    'password' => 'pass',
    'write' => [
        'host' => '主库IP',
    ],
    'read' => [
        'host' => ['从库1IP', '从库2IP'],
    ],
]

注意 read 数组里的 host 属性可以传字符串或数组。传字符串表示只有一台从库,传数组表示多台。如果各库的用户名密码不一致,也可以在 read 或 write 内单独覆盖。配置完成后,建议先跑一个查询语句确认没有报错,再进行下一步。

强制主库的查询场景

配置完成后,有一个细节需要处理:写入后立刻查询。这个场景如果被随机分配到从库,很容易看到旧数据。

如何配置ThinkPHP6的数据库读写分离

遇到写入后立刻查询的场景,可以在查询链路上调用 useMaster(true) 方法,强制本次查询走主库。例如用户提交订单后想要立即查看订单详情,如果这条查询被分配到从库,而主从复制尚未完成,用户就会看到空白页或旧数据。写法很简单:Db::name('order')->useMaster(true)->where('id', $orderId)->find()。检查是否生效,可以开启数据库日志,观察该条 SQL 实际使用的连接地址是否为主库地址。注意 useMaster 只对当前查询生效,不会影响后续请求。

在模型查询里也可以这么用,例如 Order::where('id', $orderId)->useMaster(true)->find()。不过要注意,useMaster 对关联查询不一定生效,关联模型会自动继承主模型的连接,所以如果主模型走从库,关联查询也走从库。

容易忽略的坑:分页、事务和模型查询

读写分离在查询量大的场景下有效,但有一些坑容易被刚上手的同学忽略。

第一个坑是分页查询。分页通常需要先 count 再取列表,如果这两条 SQL 被分发到不同的从库,而两个从库的数据同步进度不同,可能得到矛盾的结果,例如总页数是 10,但最后一页没有数据。这个问题可以通过强制 count 和列表使用同一个连接来规避,或者在业务上接受短时间的统计偏差。

第二个坑是事务。在事务中,所有查询都会走主库,因为事务本身已经持有了主连接。这是正确的行为,不需要额外干预。如果事务内还有写操作,那么整个事务都必须在主库完成,无法把 select 部分分离到从库。

如何配置ThinkPHP6的数据库读写分离

第三个坑是模型事件、软删除、关联预加载等查询。它们本质上还是 select,也会被路由到从库。如果这些查询对实时性敏感,需要每个查询单独考虑是否要加 useMaster(true)。另外,如果你在 beforeRead 之类的事件里做了额外查询,同样要小心。

验证配置是否生效

配置完成后,需要确认 select 是否真的落到了从库上。我一般用两种方式验证。

第一种是开启 ThinkPHP 的 SQL 日志。在 config/log.php 中设置日志级别包含 sql,然后跑几个查询,看日志里的连接信息。如果日志能输出连接名称或主机地址,就能直观看到 select 是否落在从库上。

第二种是在从库执行 SHOW PROCESSLIST;,同时在前台刷新一个简单查询页面。如果从库的进程列表里出现了这个查询,说明路由成功。注意要多刷新几次,因为从库之间有随机选择,可能不是每次都在同一台从库上。

如何配置ThinkPHP6的数据库读写分离

这里有一个检查点:注意观察日志中 select 语句是否都走从库,而 insert、update 语句是否都走主库。如果发现写语句出现在从库上,说明配置有问题,需要立即检查连接参数。

风险边界与回滚思路

读写分离只能缓解读压力,不能解决所有性能问题。在主从架构下,只有 select 查询会尝试从从库读取,join、子查询、写相关语句如 insert、update、delete 以及事务内的所有查询都会走主库。如果系统确实出现写库性能瓶颈,读写分离帮不上忙,应该考虑分库分表或引入缓存,而不是简单增加从库数量。

回滚也很简单:把 database.php 里的 read 和 write 两个数组删掉,恢复成原来的单库配置即可。因为 ThinkPHP6 的默认配置里并没有这两个键,删除后所有连接都会回到主库。建议在改动前备份原配置,并记录下当前使用的 PHP 版本和 ThinkPHP 版本,以便排查问题。

另外,如果从库出现大的同步延迟,短期的应急方案可以是临时把 read 数组中的从库地址改为主库地址,让查询全部回到主库。但这不是长久的解决办法,只能用于恢复服务。