直接用 model::get() 或 toarray() 处理万级以上数据会 oom,因每个 eloquent 模型占 1–2kb;应改用 db::table()->cursor() 返回 generator,内存恒定 kb 级,或手动 yield 实现轻量 hydrate。

直接用 Model::get() 或 $model->toArray() 处理万级以上数据,必然触发 OOM;根本原因不是 PHP 数组大,而是 Eloquent 模型实例本身带完整属性容器、变更跟踪、关系缓存和访问器逻辑——每个模型至少占用 1–2KB,10 万条轻松吃掉 100MB+ 内存。
为什么 toArray() 会放大内存问题
很多人以为 toArray() 只是“转个格式”,其实它会强制触发整个模型的 hydration 流程:包括加载隐藏字段、调用 getAttributes()、遍历所有 casts、执行 append 属性的 getter 方法,甚至递归处理 relations(哪怕你没显式调用 load(),只要在 toArray() 前访问过关系,缓存就已存在)。
常见错误现象:Fatal error: Allowed memory size of 134217728 bytes exhausted;Web 接口返回 500 但日志空白;CLI 脚本跑一半卡死且 memory_get_usage() 显示不高——说明泄漏发生在 C 层或协程栈,不是 PHP 堆。
-
toArray()是模型生命周期终点操作,一旦调用,所有中间状态全被固化进内存 - 即使你只取
id和name,Eloquent 仍会 hydrate 完整模型对象 - 搭配
collect($models)->map(...)->toArray()更危险:先生成对象数组,再转集合,再映射,最后再转数组——三重冗余加载
绕过 Model 直接查表 + cursor() 返回 Generator
最有效方案是彻底切断 Eloquent 的模型装配链路,改用原生查询 + 游标式流读。Hyperf 默认已关闭 MySQL 缓冲(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false),所以 cursor() 能真正逐行 fetch。
- 用
Db::table('users')->select('id', 'name', 'status')->cursor()替代User::select('id', 'name')->get() - 返回的是
Generator,不是Collection,无法调用first()、filter()等方法,但内存恒定在 KB 级 - 若需类型提示,函数必须声明返回
Generator,不能写iterable,否则 PHP 8.2+ 报错且 IDE 失去 yield 流程感知 - 别把
cursor()结果包进collect()或赋值给变量,foreach ($cursor as $row)是唯一安全用法
必须用 Model 时的手动 yield 方案
极少数场景(如需要自动 cast、日期格式化、访问器逻辑)不得不走 Model,那就得自己控制 hydrate 过程:不 new 模型,只复用构造逻辑中的字段映射能力。
示例函数:
function getUsersAsArrayCursor(): Generator
{
$stmt = Db::getPdo()->prepare('SELECT id, name, created_at FROM users WHERE status = ?');
$stmt->execute([1]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// 手动应用 cast:如 created_at -> Carbon
if (isset($row['created_at'])) {
$row['created_at'] = date('Y-m-d H:i:s', strtotime($row['created_at']));
}
yield $row;
}
}
- 函数内禁止
use ($this),防止闭包捕获 DI 容器或上下文大对象 - 避免在循环中调用
new User()或User::hydrate(),那又回到全量加载老路 - 每处理 500 行后加一次
gc_collect_cycles(),尤其在协程长任务中(比如导出接口) - MySQL 下确保
SELECT字段含唯一递增主键(如id),否则cursor()可能漏数据
容易被忽略的协程与 GC 配合点
Generator 本身不保内存安全,它只是个语法糖;真正在 Hyperf 里压住 RSS 的,是协程生命周期管理 + 主动 GC 触发时机。
-
memory_get_usage()看不到 Swoole 协程栈、PDO C 层 malloc、Monolog 缓冲区——用ps -o rss= -p $(pgrep -f "hyperf")查真实内存 - Worker0 RSS 异常飙升?大概率是首个请求触发了长协程且没设超时,或
Context::set()后没配defer清理 -
max_request => 0是隐形杀手:必须设为100000左右,配合reload_async => true实现平滑重启 - Generator 函数末尾显式
unset($bigArray)或delContext 键,比等 GC 更可靠











