关于一个 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

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

《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 10

数据大了使用chunk

3年前 评论
flc1125 3年前

推薦前幾天看到的好文章

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

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

3年前 评论
hezhizheng

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

3年前 评论
Nella 3年前

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

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

3年前 评论

试试把这个设置为false

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

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

3年前 评论

要用 chunkById()

3年前 评论

chunkById,更适用吧

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!