DB::transaction() 在读写分离下仍可能在从库执行,因Laravel事务不自动切主库,需显式指定连接名如 DB::connection('mysql_write')->transaction(...)。

读写分离下 DB::transaction() 为什么还在从库执行?
因为 Laravel 的事务默认不强制走主库,即使你配置了读写分离,DB::transaction() 内部的查询仍可能被路由到从库——只要没显式指定连接名或触发写操作前置。
常见错误现象:Illuminate\Database\TransactionException 报 “can't execute statement in a read-only transaction”,或更新后立刻查不到新数据(从库延迟导致)。
- 事务开始前,Laravel 不会自动切换到主连接,它只在第一次写操作时才“感知”需主库,但此时从库连接已建立,部分驱动(如 MySQL PDO)会拒绝后续写入
- 如果你在事务里先执行
DB::table('users')->where(...)->first()(读),再->update()(写),读操作可能落在从库,而写操作因连接未复用失败或降级异常 - 解决方法:显式指定主连接,
DB::connection('mysql_write')->transaction(...);或者确保事务内第一个查询就是写操作(不推荐,不可靠)
DB::select() 和 DB::table()->get() 路由行为差异
二者底层都走查询构建器,但路由时机不同:前者绕过查询构建器的连接推断逻辑,直接使用默认连接(通常是读连接);后者会根据方法链是否含写意图(如 insert()、update())动态选连接。
使用场景:你想查配置表且必须实时(比如开关状态),但用了 DB::select("SELECT * FROM features WHERE key = ?"),结果读到旧值——因为没触发写意图,Laravel 默认扔给从库。
-
DB::select()总是走config('database.connections.mysql_read')(或默认连接),除非你传['connection' => 'mysql_write'] -
DB::table('features')->where('key', 'pay_enabled')->first()同样走从库,但DB::table('features')->where('key', 'pay_enabled')->update(...)会自动切主库 - 参数差异:没有“强制主库”参数,只能换连接名或改全局默认连接(
DB::setDefaultConnection('mysql_write'),慎用)
延迟容忍阈值怎么设?seconds_since_master 不是万能解
Laravel 本身不提供主从延迟检测,所谓“延迟容忍”得自己实现,而依赖 Seconds_Behind_Master 有硬伤:MySQL 5.7+ 在并行复制下该值可能为 0 却仍有延迟;GTID 模式下甚至不更新这个字段。
性能影响:每次查询前都去主库查 SHOW SLAVE STATUS,IO 和网络开销大,且无法缓存。
- 更可行的做法是按业务分级:用户中心类强一致性操作(如登录态变更)跳过从库,直接走主连接;列表页、统计类允许秒级延迟,走从库
- 不要在中间件里统一判断延迟再路由,容易成为性能瓶颈;应在 Repository 或 Service 层按需指定连接
- 如果真要监控延迟,建议用外部工具(如 Prometheus + mysqld_exporter)聚合后供 Laravel 服务异步拉取,而非每次请求都查
强制主库的三种可靠方式,别碰 DB::unprepared()
DB::unprepared() 是执行原生语句的兜底方法,但它不参与连接路由逻辑,也不受读写分离配置约束——这意味着你写了 DB::unprepared("UPDATE ..."),却可能仍在从库上执行并报错。
真正有效的强制主库手段只有这些:
- 用连接名:
DB::connection('mysql_write')->table('orders')->where('id', 123)->update([...]) - 临时切换默认连接:
DB::setDefaultConnection('mysql_write'); $result = DB::table(...)->get(); DB::setDefaultConnection('mysql_read');(注意并发安全,别在中间件里这么干) - 模型里指定连接:
class Order extends Model { protected $connection = 'mysql_write'; },适合整张表都要求强一致的场景
容易被忽略的是:Laravel 的连接池和长连接复用会让连接名切换变得“粘滞”,尤其在 Swoole 或 Octane 环境下,DB::reconnect() 可能不生效,最稳妥的是每次明确调用 DB::connection('xxx')。


















