Laravel读写分离需在config/database.php中将read/write嵌套于同一连接配置内,事务内所有查询强制走写库,DB::select()等纯SELECT走读库,主从延迟需业务层容忍或缓存解决。

数据库读写分离配置必须改 config/database.php 的 connections 结构
Laravel 本身不自动区分读/写,靠的是你手动把主库(写)和从库(读)定义成一个逻辑连接,框架内部根据查询类型自动路由。关键不是加几个新连接,而是重构 mysql 这个 connection 的结构。
常见错误是只加了 mysql_read 和 mysql_write 两个独立连接,然后在代码里手动选——这不算读写分离,只是手动切换,事务、连接复用、故障转移全得自己兜底。
- 必须把
'read'和'write'作为子键嵌套进同一个 connection(比如mysql)里,Laravel 的Connection类才能识别 -
read下可配多个从库,用数组形式,Laravel 默认轮询;write只能有一个,且必须是关联数组(哪怕只有一个库也要写成['host' => '...']) - 所有读写共用的配置(如
driver、database、username、password)要提到外层,不能重复写在 read/write 里,否则会被忽略
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_CERT => env('MYSQL_ATTR_SSL_CERT'),
PDO::MYSQL_ATTR_SSL_KEY => env('MYSQL_ATTR_SSL_KEY'),
]) : [],
'read' => [
['host' => env('DB_READ_HOST_1', '192.168.1.10')],
['host' => env('DB_READ_HOST_2', '192.168.1.11')],
],
'write' => [
'host' => env('DB_WRITE_HOST', '192.168.1.5'),
],
],
事务内所有查询都会走写库,哪怕显式调用 DB::connection()->table()->select()
这是最容易被忽略的逻辑陷阱:只要当前请求进了事务(DB::transaction 或 DB::beginTransaction()),后续所有查询——包括你写的 DB::table('users')->where('id', 1)->first()——都会强制发到写库,无视是否是 SELECT。
原因很简单:Laravel 要保证事务一致性,不能让读操作落到可能延迟的从库上。这点和很多 ORM 的“强制读主”行为一致,不是 bug,是设计使然。
- 如果你在事务里做大量只读校验(比如查用户余额、查订单状态),这些查询不会被分流,反而加重主库压力
- 想绕过?不行。Laravel 没提供事务中临时切读库的 API;硬切(比如
DB::connection('mysql_read'))会脱离当前事务上下文,导致数据不一致 - 真正可行的解法是:提前把事务前需要的读数据取好,或者把非强一致的读操作挪到事务外
DB::select() 和原生查询默认走读库,但 DB::statement() 和 DB::unprepared() 一定走写库
框架对查询类型的判断非常机械:只看 SQL 字符串开头是不是 SELECT(忽略大小写和空格)。所以不是所有“看起来像读”的语句都会被分到从库。
典型反例是 SHOW TABLES、EXPLAIN SELECT ...、WITH ... SELECT ...(某些旧版 MySQL 不识别 WITH 开头为读),这些都可能被误判或直接 fallback 到写库。
-
DB::select():走读库(前提是 SQL 是纯 SELECT) -
DB::insert()/DB::update()/DB::delete():走写库 -
DB::statement('CREATE TABLE ...'):走写库(即使没写 INSERT/UPDATE) -
DB::unprepared('SET @var = 1'):走写库(任何非查询类语句) - 注意
DB::raw()本身不决定路由,它只是字符串包装器,最终取决于它被用在哪个方法里
读库延迟导致数据不一致,不能只靠重试,得从业务层设容忍窗口
MySQL 主从延迟是物理现实,Laravel 的读写分离不解决延迟问题,只解决流量分发。你在写完一条记录后立刻用 DB::table()->find() 查,大概率查不到——因为从库还没同步过来。
这不是配置问题,也不是 Laravel 的锅,是分布式系统的基本约束。强行加 sleep(0.1) 或循环查,线上扛不住;用 DB::connection('mysql')->table() 强制读主,又失去分流意义。
- 对强一致性场景(如支付结果页、密码修改后立即登录),直接读主库,用
DB::connection('mysql.write')显式指定 - 对弱一致性场景(如文章列表、评论数),接受几秒延迟,别做“写完立刻查”这种假设
- 如果业务真需要写后即读(Read-Your-Writes),得引入缓存层(如 Redis 记录刚写入的 ID)或使用 GTID +
WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS(仅限 MySQL 5.7+,运维成本高)
读写分离不是银弹,它放大了你对数据一致性的认知盲区。配置对了,只是开始。










