商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > ThinkPHP 8.0 生成器(Generator)大数据导出【省内存】

ThinkPHP 8.0 生成器(Generator)大数据导出【省内存】

  发布于2026-07-15 阅读(0)

扫一扫,手机访问

ThinkPHP 8.0 导出百万级用户数据时,内存瞬间飙到 512MB 并报错“Allowed memory size exhausted”——这问题其实不怪服务器,问题出在你还用着 Db::select() 把整张表拖进内存再 foreach。好比把长江水抽进玻璃杯,不炸才怪。正确的做法是改用生成器(Generator)流式处理,让数据按需吐出,而不是一次性加载。

ThinkPHP 8.0 生成器(Generator)大数据导出【省内存】

用生成器替代全量查询

第一步,改写数据获取逻辑,让数据库结果集按需返回,而不是一股脑全塞进内存。在控制器或服务类中,定义一个生成器方法:

public function userGenerator(): Generator
{
  $sql = "SELECT id, name, email, created_time FROM user ORDER BY id";
  $pdo = $this->app->db->getPdo();
  $stmt = $pdo->prepare($sql);
  $stmt->execute();
  while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    yield $row;
  }
}

注意:这里必须用原生 PDO 的 unbuffered query(未缓冲查询),否则 fetch() 仍然会预加载全部结果。ThinkPHP 默认的 Db::query() 是缓冲模式,不能直接 yield。

第二步,调用该生成器导出 CSV,每处理一行就释放变量引用:

header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="users_' . date('YmdHis') . '.csv"');
$fp = fopen('php://output', 'wb');
fputcsv($fp, ['ID', '姓名', '邮箱', '注册时间']);
foreach ($this->userGenerator() as $user) {
  // 关键:避免在循环内拼接大字符串或存入数组
  fputcsv($fp, [$user['id'], $user['name'], $user['email'], $user['created_time']]);
  // 手动触发 GC 清理潜在残留(尤其含闭包或对象时)
  if (function_exists('gc_collect_cycles')) gc_collect_cycles();
}
fclose($fp);

ThinkPHP 原生支持的 Generator 封装方案

方法一:用 Db::cursor()(TP8.0+ 内置,推荐)

这是 ThinkPHP 8.0 官方提供的流式游标接口,底层已启用 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false,无需手写 PDO。

$cursor = Db::table('user')
  ->field('id,name,email,created_time')
  ->order('id')
  ->cursor();
foreach ($cursor as $user) {
  fputcsv($fp, [$user['id'], $user['name'], $user['email'], $user['created_time']]);
}

需要提醒的是:Db::cursor() 不支持 where 条件链式调用后直接加 ->limit()——它只接受原始 SQL 或简单条件,复杂查询请改用原生 PDO + yield 方案。

方法二:封装成可复用的 Generator 工具类

新建 app/common/Generator.php

namespace app\common;
use think\facade\Db;
class Generator
{
  public static function fromQuery(string $sql, array $params = []): Generator
  {
    $pdo = Db::getPdo();
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
      yield $row;
    }
  }
}

控制器中调用:foreach (Generator::fromQuery("SELECT * FROM user WHERE status = ?", [1]) as $u) { ... }

导出前必须关闭的三项配置

① 关闭 APP_DEBUG:.env 中设 APP_DEBUG=false,否则调试组件和 trace 日志会额外吃掉 80MB+ 内存。

② 禁用日志写文件:config/log.php 中将驱动改为 'type' => 'socket''type' => 'redis',避免高频 fputcsv 期间被 Log::info() 阻塞。

③ 清空 Schema 缓存并重生成:执行 php think schema:clear && php think schema:build,旧缓存可能残留冗余字段映射,导致 ORM 自动附加无关关联数据。

本文转载于:https://www.php.cn/faq/2822378.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

上一篇:Swoole中协程并发Map操作配置

下一篇:T

热门关注