最稳妥的只读模式需数据库权限控制与应用层sql拦截双管齐下:生产环境移除db账号写权限,应用层在appserviceprovider中装饰pdo::prepare方法,正则匹配insert/update/delete等写操作并抛异常,同时停用队列进程并加前置只读检查。

如何在 Laravel 中临时启用数据库只读模式
直接改 DB::connection()->getDoctrineConnection() 不起作用,Laravel 的底层连接是懒加载且带缓存的,必须从连接配置或运行时状态两个层面同时干预。
- 最稳妥的方式是在
.env中临时设置DB_CONNECTION=sqlite并指向一个只读 SQLite 文件(仅限开发/测试),但生产环境不适用 - 生产环境应走 MySQL 层面控制:给 Laravel 使用的数据库账号移除
INSERT/UPDATE/DELETE权限,再配合应用层拦截 - 不要依赖
DB::statement('SET SESSION transaction_read_only = ON')—— 它只对当前连接生效,而 Laravel 的连接池会复用连接,下次请求可能拿到未设只读的连接
Laravel 应用层拦截写操作的可靠写法
靠中间件或全局事件监听 eloquent.updating 这类钩子不可靠:Eloquent 不覆盖原生 DB::table()->update()、DB::insert() 或原始 SQL,这些会绕过所有模型事件。
- 推荐在
AppServiceProvider::boot()中重写DB::connection()->getPdo()返回的 PDO 实例,用装饰器包装其prepare()方法,对含INSERT、UPDATE、DELETE、REPLACE、ALTER、DROP的 SQL 抛出RuntimeException - 注意大小写:MySQL 默认不区分关键字大小写,正则匹配需加
i标志,如/^(insert|update|delete|replace|alter|drop)/i - 避免误杀:跳过以
--、/*开头的注释行,以及包含SELECT但混有写操作的复合语句(如INSERT ... SELECT)需单独识别
维护期间如何避免队列任务写库
队列消费者进程独立于 Web 请求生命周期,.env 变更或中间件对其完全无效。如果队列还在执行写操作,只读就形同虚设。
- 停掉所有
php artisan queue:work进程,改用php artisan queue:listen --once模式逐条检查,或直接php artisan queue:restart后清空队列 - 在
config/queue.php的连接配置中加入'retry_after' => 1,让失败任务快速退出,避免卡在写库逻辑里 - 关键队列(如支付回调、订单同步)建议加前置检查:任务 handle() 开头调用
if (config('app.read_only')) { throw new RuntimeException('Maintenance mode'); }
为什么不能只靠 DB::transaction() 或 DB::beginTransaction()
DB::transaction() 是事务控制,不是权限控制。它既不限制语句类型,也不阻止多个事务并发写入 —— 维护期真正要防的是「任何写行为发生」,而不是「写得不一致」。
- 事务内照样能执行
UPDATE users SET name = ?,只读模式的目标是让这句根本跑不起来 -
DB::beginTransaction()甚至不保证开启新事务(PDO 默认 auto-commit),在某些驱动下只是空操作 - 试图在事务开始前检测只读状态并抛异常?不行 —— Laravel 的事务启动时机晚于查询构造,
DB::table('x')->where(...)->update(...)在update()调用时才真正生成 SQL,此时事务早已开始
真正的只读必须落在 SQL 发送到数据库之前那一步,要么数据库账号没权限,要么应用层在 prepare 阶段就截住。其他都是障眼法。











