发布于2026-07-15 阅读(0)
扫一扫,手机访问
ThinkPHP 8.0 导出百万级用户数据时,内存瞬间飙到 512MB 并报错“Allowed memory size exhausted”——这问题其实不怪服务器,问题出在你还用着 Db::select() 把整张表拖进内存再 foreach。好比把长江水抽进玻璃杯,不炸才怪。正确的做法是改用生成器(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 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 方案。
新建 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 自动附加无关关联数据。
下一篇:T
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8