根本原因是Eloquent模型实例、查询构建器缓存、监听器引用和冗余序列化数据持续累积未释放;应改用cursorPaginate、禁用事件、显式unset、DB::unprepared批量操作及启用zlib/msgpack压缩缓存。

高并发下 Laravel 内存飙升,根本原因不是请求多,而是每个请求在处理过程中不断累积未释放的资源——Eloquent 模型实例、查询构建器缓存、监听器引用、冗余序列化数据,都在悄悄吃掉内存。关键不是“压测时调大 memory_limit”,而是从代码执行链路里把内存泄漏点掐断。
用 cursorPaginate 替代 chunk 处理大数据集
常见错误现象:chunk() 在高并发循环中越跑越慢、内存不降反升,memory_get_usage() 每批涨 2–5MB。这是因为 chunk() 内部复用同一个查询构建器实例,并持续持有已加载模型的引用,尤其配合 with() 时更严重。
- 改用
cursorPaginate():它基于游标(主键值)分页,不依赖OFFSET,也不缓存构建器状态,每批都是全新轻量查询 - 手动推进游标时,显式 unset 模型变量:
unset($user);,避免 PHP 引用计数延迟回收 - 若需按非主键字段排序,先建联合索引(如
(status, id)),再用cursorPaginate(100, ['id', 'name'], 'id')
禁用 Eloquent 监听器与事件广播
使用场景:后台命令、队列任务、API 批量导入等非用户交互流程。默认开启的 eloquent.* 事件监听器会为每个模型操作触发完整生命周期钩子,哪怕你没注册任何监听器,框架仍预留了回调栈空间。
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
- 在 Artisan 命令开头加:
Event::forget('eloquent.*'); - 临时关闭模型事件:
User::withoutEvents(fn() => User::create([...]) ); - 生产环境配置
APP_DEBUG=false同时确保LOG_LEVEL=error,避免调试日志对象长期驻留内存
批量操作绕过 Eloquent,直连 PDO
错误写法:User::whereIn('id', $ids)->update(['status' => 'processed']);——它仍会实例化 Builder、走访问器、触发 updating 事件,内存开销是原生 SQL 的 3 倍以上。
- 用
DB::unprepared()发裸 SQL:DB::unprepared("UPDATE users SET status = 'processed' WHERE id IN (" . implode(',', $ids) . ")"); - 对超大 ID 列表,先
array_chunk($ids, 500)分组,避免超出 MySQLmax_allowed_packet - 执行后清空连接预处理缓存:
DB::connection()->getPdo()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
缓存值启用 zlib 压缩或 msgpack 序列化
问题根源:Redis 中一个含 100 条记录的 Eloquent 集合缓存,未压缩时占 1.2MB;PHP serialize() 本身冗余度高,且不压缩直接存,等于把内存压力转嫁给 Redis 内存和网络带宽。
- Laravel 13+ 直接在
config/cache.php的 redis store 中加:'serializer' => 'zlib'(需php -m | grep zlib确认扩展启用) - 或改用
'serializer' => 'msgpack'(需pecl install msgpack),对结构化数据压缩率更高,且无 zlib 头开销 - 小键值(如 gzcompress('ok') 反而变大;可自定义 Store 做阈值判断,但多数项目直接开
zlib就够用
真正难的不是选哪个方案,而是意识到:Laravel 的内存问题从来不在“单次请求太重”,而在“每次请求都悄悄留下一点没清理”。比如一个忘了 unset() 的集合变量、一次没关的事务、一个始终开着的全局监听器——它们在并发下被放大,最终让内存使用曲线变成一条爬升不止的直线。

















