thinkphp 8.0 导出百万级用户数据应使用生成器流式处理避免内存溢出:优先用 db::cursor(),次选原生 pdo yield,禁用调试、日志写文件及清理 schema 缓存。

ThinkPHP 8.0 中导出百万级用户数据时,内存瞬间飙到 512MB 并报错“Allowed memory size exhausted”,不是服务器配得低,而是你还在用 Db::select() 把整张表拖进内存再 foreach —— 这等于把长江水抽进玻璃杯,不炸才怪。
用生成器替代全量查询
第一步:改写数据获取逻辑,让数据库结果集按需吐出,而非一次性加载。
在控制器或服务类中,定义一个生成器方法:
public function userGenerator(): \Generator<br>{<br> $sql = "SELECT id, name, email, created_time FROM user ORDER BY id";<br> $pdo = $this->app->db->getPdo();<br> $stmt = $pdo->prepare($sql);<br> $stmt->execute();<br> while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {<br> yield $row;<br> }<br>}
注意:必须用原生 PDO 的 unbuffered query(未缓冲查询),否则 fetch() 仍会预加载全部结果;ThinkPHP 默认的 Db::query() 是缓冲模式,不能直接 yield。
第二步:调用该生成器导出 CSV,每行处理完立即释放变量引用。
header('Content-Type: text/csv; charset=utf-8');<br>header('Content-Disposition: attachment; filename="users_' . date('YmdHis') . '.csv"');<br>$fp = fopen('php://output', 'wb');<br>fputcsv($fp, ['ID', '姓名', '邮箱', '注册时间']);<br>foreach ($this->userGenerator() as $user) {<br> // 关键:避免在循环内拼接大字符串或存入数组<br> fputcsv($fp, [$user['id'], $user['name'], $user['email'], $user['created_time']]);<br> // 手动触发 GC 清理潜在残留(尤其含闭包或对象时)<br> if (function_exists('gc_collect_cycles')) gc_collect_cycles();<br>}<br>fclose($fp);
ThinkPHP 原生支持的 Generator 封装方案
方法一:用 Db::cursor()(TP8.0+ 内置,推荐)
这是 ThinkPHP 8.0 官方提供的流式游标接口,底层已启用 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false,无需手写 PDO。
$cursor = Db::table('user')<br> ->field('id,name,email,created_time')<br> ->order('id')<br> ->cursor();<br>foreach ($cursor as $user) {<br> fputcsv($fp, [$user['id'], $user['name'], $user['email'], $user['created_time']]);<br>}
【Db::cursor() 不支持 where 条件链式调用后直接加 ->limit()】——它只接受原始 SQL 或简单条件,复杂查询请改用原生 PDO + yield 方案。
方法二:封装成可复用的 Generator 工具类
新建 app/common/Generator.php:
namespace app\common;<br>use think\facade\Db;<br>class Generator<br>{<br> public static function fromQuery(string $sql, array $params = []): \Generator<br> {<br> $pdo = Db::getPdo();<br> $stmt = $pdo->prepare($sql);<br> $stmt->execute($params);<br> while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {<br> yield $row;<br> }<br> }<br>}
控制器中调用: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 自动附加无关关联数据。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











