Hyperf 3.1 中模型 restore() 在协程环境下易出问题,主因是调用方式、事件未触发、并发无锁、事务缺失及协程生命周期干扰;需手动触发 restored 事件、用 where('deleted_at', '!=', null)->update() 或行锁、包裹 DB::transaction()、避免阻塞I/O。

Hyperf 3.1 中模型软删除的 restore() 在协程环境下容易出问题,不是因为方法本身不兼容,而是调用方式、事件触发和事务上下文没对齐协程特性,导致恢复失败、状态未重置、日志断链或数据不一致。
协程中 restore() 不触发 restored 事件
Hyperf 默认的 restore() 和 Laravel 一样,只更新 deleted_at 字段,不触发任何模型事件(包括 restored)。在协程服务中,你常依赖事件做缓存清理、状态重置或日志记录,但直接调用 $model->restore() 后,监听器完全静默。
- 不能靠
$model::observe()或boot()里注册restored就完事——它根本不会被调用 - Hyperf 3.1 没有
restoreWithEvents()(那是 Laravel 10.29+ 的功能),必须手动补全 - 推荐做法:恢复后立即手动触发事件,例如
$model->fireModelEvent('restored', false),再在监听器里执行重置逻辑
并发恢复时 deleted_at 字段被覆盖或冲突
多个协程同时操作同一条软删记录(比如两个请求几乎同时调用 restore()),可能因无锁机制导致 deleted_at 被反复设为 NULL,甚至出现数据库层面的乐观锁失效或更新丢失。
- 避免裸调用
restore(),改用带条件的原子更新:UserModel::where('id', $id)->where('deleted_at', '!=', null)->update(['deleted_at' => null]) - 如需强一致性,加数据库行锁:
UserModel::lockForUpdate()->where('id', $id)->onlyTrashed()->first(),再调用restore() - 注意:Hyperf 的 ORM 在协程中复用连接,
lockForUpdate有效,但必须确保没跨协程混用同一 Model 实例
事务包裹缺失,关联操作不同步
协程服务中常把恢复和权限重开、积分返还、消息通知等串在一起。若没显式开启事务,某一步失败会导致 deleted_at 已清空,但后续逻辑中断,数据处于中间态。
-
restore()自身不开启事务,必须用DB::transaction()包裹整条链路 - 错误写法:
$user->restore(); AuditLog::create([...]);—— 无事务,审计日志失败时用户已恢复 - 正确写法:
DB::transaction(function () use ($user) { $user->restore(); AuditLog::create([...]); }); - 批量恢复时用
chunkById()分片 + 每批独立事务,防超时和内存溢出
协程生命周期干扰 restore() 执行流
在 go() 或 co() 启动的协程中,如果恢复操作耗时较长(如含远程 HTTP 请求或复杂计算),可能被协程调度器中断,或因父协程提前结束导致子协程被回收,造成恢复不完整。
- 不要在协程里嵌套调用阻塞式 I/O(如原生
file_get_contents)来辅助恢复逻辑 - 所有外部调用必须协程安全:HTTP 用
Hyperf\HttpClient\Client,Redis 用Hyperf\Redis\Redis - 关键恢复步骤建议放在主协程或通过
Parallel显式管理,避免隐式调度风险



















