关于一个 Laravel 使用 cursor 占用内存问题

背景#

我需要把一张表的所有数据查询出来,然后逐一查询出来进行逻辑处理。
代码如下:

    public function handle()
    {
        EloquentStoredEvent::query()->cursor()->each(function (EloquentStoredEvent $storedEvent){
            // 逻辑处理
            $this->info(round(memory_get_usage()/1024/1024, 2).'MB');
        });
    }

在代码中,我使用了 Eloquent 的 cursor, 他的原理就是使用了 PDO 的 execute 方法,将数据库的结果集放到 php 的内存中,然后通过 php 的生成器 一行一行从结果集 fetch 出来数据转换成 orm 对象。这个比起使用 Eloquent 的 get 更能节约内存开销。

问题#

当我执行代码的时候。它会一行行执行

$this->info(round(memory_get_usage()/1024/1024, 2).'MB');

我发现了个问题,假如有 80 万条数据,第一次执行内存占用 962.36MB,后面会慢慢增加,到最后一次执行内存占用 1901.39MB
那如果有一个脚本需要长时间才能处理完成,你开始观察几分钟可能正常,但是看你过了半个小时它就因为内存不够而运行失败。

找到原因#

后面我发现和 PDO::ATTR_EMULATE_PREPARES 有关系,在 Illuminate\Database\Connectors\Connector 默认 PDO::ATTR_EMULATE_PREPARES => false,当它为 false 一行行 fetch 才会动态增长。为 true 内存就是固定值。

测试代码#

<?php

ini_set('memory_limit', '3024M');

class Db{
    public $dbh;

    public function __construct()
    {
        $this->dbh = new PDO('mysql:host=127.0.0.1;port=3306;dbname=test', 'root', '',
            [
                PDO::ATTR_CASE => PDO::CASE_NATURAL,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
                PDO::ATTR_STRINGIFY_FETCHES => false,
                PDO::ATTR_EMULATE_PREPARES => true,
            ]);

    }

    public function fetch()
    {
        $sth = $this->dbh->prepare("select * from `stored_events`");

        $sth->setFetchMode(PDO::FETCH_OBJ);
        $sth->execute();

        while ($record = $sth->fetch()) {
            yield $record;
        }
    }
}

$db = new Db();
var_dump(round(memory_get_usage()/1024/1024, 2).'MB');

foreach ($db->fetch() as $v) {
    var_dump(round(memory_get_usage()/1024/1024, 2).'MB');
};
var_dump(round(memory_get_usage()/1024/1024, 2).'MB');

在测试代码中,
当我将 PDO::ATTR_EMULATE_PREPARES 置为 true 时候,foreach 循环打印的是 1910.33MB
当我将 PDO::ATTR_EMULATE_PREPARES 置为 false 时候,foreach 循环打印的是从 931.52MB 增长到 1870.55MB

有哪位大神对这一块比较了解的,能解答下吗?

《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 10

数据大了使用 chunk

4年前 评论
flc1125 4年前

推薦前幾天看到的好文章

Laravel — 18 種優化資料查詢效能的方式

裡面有提到當資料量一大的時候建議使用 chunk 撈取資料

4年前 评论
hezhizheng

@Nella 好奇 "捞" 这个词的由来 ,身边几乎所有同事都是用 "查" ,貌似内地某些大厂也有用 "捞" 的说法,还是只是两岸文化用词差异而已 :joy:

4年前 评论
Nella 4年前

@Imuyu @Nella 谢谢回答。目前还无法使用 chunk , 因为我也是使用的另外一个拓展包,拓展包的底层用的是 cursor。后续估计也会改成 chunk。

我好奇的地方是 PDO::ATTR_EMULATE_PREPARES = false 从 pdo fetch 数据的时候,为什么内存消耗会增加?他的原理我有点没太懂。按道理 fetch 一条数据给 foreach 的 $v。然后变量一直都只有一个。在测试代码我看不到有内存会增加的地方。

4年前 评论

试试把这个设置为 false

PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false;
4年前 评论
游离不2

官方也有人提了 issue,可以看看 github.com/laravel/framework/issue...

4年前 评论

要用 chunkById ()

4年前 评论

chunkById,更适用吧

4年前 评论