observer中改模型会触发自身再次监听,导致递归调用和mysql死锁;应避免在回调中调用save/update/touch,改用属性赋值或队列任务,拆解跨模型闭环依赖,并禁用内部查询。
observer里改模型会触发自身再次监听
这是最常见死锁源头:在 updating 或 saving 回调里调用 $model->save() 或 $model->update(),会重新进入同一 Observer 的监听链,形成递归调用。Laravel 不做递归拦截,数据库事务没提交前,同一行记录被反复加锁,MySQL 报 Deadlock found when trying to get lock 或直接卡住。
- 避免在 Observer 方法内主动调用
save()、update()、touch() - 改用
$model->setRawAttributes()或直接赋值属性(如$model->updated_by = 1),让当前保存操作一并写入 - 若必须触发另一次持久化(比如同步日志表),改用队列任务
dispatch(new SyncLogJob($model)),脱离当前事务上下文
事务中多个 Observer 互相修改引发锁等待
比如 UserObserver 修改了 last_login_at,同时 ProfileObserver 监听 User 更新后去更新关联的 Profile 记录,而 ProfileObserver 又触发了 User 关联字段反向更新——这种跨模型 Observer 链容易让事务持有多个行锁时间过长,MySQL 等待超时后终止进程,报错 Lock wait timeout exceeded。
- 检查
App\Observers\下所有 Observer 是否存在「监听 A → 更新 B → B 触发监听 → 反向更新 A」的闭环 - 对非核心字段(如统计类、缓存标记)改用
DB::transactionLevel() > 0判断是否在事务中,事务内跳过敏感更新 - 把跨模型副作用逻辑移到
created/updated(已提交后)事件,或用afterCommit包裹(Laravel 9+)
Observer 中执行 DB 查询或 Eloquent 操作加重锁竞争
在 saving 回调里写 User::where(...)->first() 或 $model->relation()->sync(),不仅可能触发 N+1,更关键的是这些查询会复用当前事务连接,在高并发下加剧行锁/间隙锁争抢,尤其配合 SELECT ... FOR UPDATE 类操作时极易死锁。
- 禁用 Observer 内部任何
Model::query()、$model->load()、$model->refresh() - 需要读取关联数据?提前在控制器或服务层查好,通过
with()或显式传参带入 Observer(如$model->setAppended('current_user_role')) - 确认 MySQL 隔离级别是
REPEATABLE READ(Laravel 默认),避免升级到SERIALIZABLE加重锁压力
验证 Observer 是否真正在循环触发
光看日志不够,得抓实际调用栈。Laravel 的 Observer 是事件驱动,但底层绑定在 Model 事件上,一旦有第三方包(如 spatie/laravel-activitylog)也监听相同事件,就可能叠加触发。
- 临时在 Observer 方法开头加
if (app()->environment('local')) { \Log::debug('UserObserver@updating', ['trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5)]); } - 检查
Event::getListeners('eloquent.updating: App\Models\User')输出,确认是否注册了重复或隐式监听器 - 运行
php artisan event:clear并重启队列,排除 opcache 或事件缓存导致的监听残留
Observer 的静默性让它很难被第一时间怀疑——它不报错、不抛异常,只在并发压测或特定数据组合下突然卡死。真正麻烦的不是写法,而是它藏在业务逻辑深处,和事务、连接池、MySQL 锁机制咬合在一起,稍不注意就变成定时炸弹。











