核心选型 openspout/openspout — 流式写入内存恒定 ~10MB无需加载整个文档到内存。 --- 架构总览 HTTP请求 → 异步队列 → 自定义进程(Worker)→ 分页游标查询 → 流式写XLSX → OSS/本地 ↑ ↑ 立即返回task_id 每批释放内存协程并发拉取 --- 一、进程模型?php // config/autoload/processes.phpreturn[Hyperf\AsyncQueue\Process\ConsumerProcess::class, App\Process\ReportExportProcess::class,];?php // app/Process/ReportExportProcess.php namespace App\Process;use Hyperf\Process\AbstractProcess;use Hyperf\Process\Annotation\Process;use Hyperf\Coroutine\Coroutine;use Swoole\Coroutine\Channel;#[Process(name: report-export, nums: 4, enableCoroutine: true)]class ReportExportProcess extends AbstractProcess{// 背压控制最多8个协程同时渲染防止内存爆炸 private Channel$semaphore;publicfunctionhandle(): void{$this-semaphorenew Channel(8);while(true){$task$this-popTask();if(!$task){Coroutine::sleep(0.5);continue;}$this-semaphore-push(1);// 占槽 Coroutine::create(function()use($task){try{(new ReportExporter($task))-run();}finally{$this-semaphore-pop();// 释放槽}});}}}▎ 策略nums4进程数CPU核数每进程8协程并发Channel 做背压。 ▎ 总并发32远低于连接池上限避免排队。 --- 二、内存管理?php // app/Export/ReportExporter.php namespace App\Export;use OpenSpout\Writer\XLSX\Writer;use OpenSpout\Writer\XLSX\Options;use OpenSpout\Common\Entity\Row;use OpenSpout\Common\Entity\Style\Style;use Hyperf\DbConnection\Db;class ReportExporter{private const BATCH5000;// 每批行数可调 publicfunction__construct(privatereadonlyarray$task){}publicfunctionrun(): void{$pathsprintf(/tmp/reports/%s.xlsx,$this-task[id]);$optionsnew Options();$options-DEFAULT_ROW_STYLE(new Style())-setShouldWrapText(false);$writernew Writer($options);$writer-openToFile($path);// 表头$writer-addRow(Row::fromValues($this-task[headers]));// 分页游标写入内存始终 O(BATCH)foreach($this-cursorPages()as$rows){$writer-addRows(array_map(fn($r)Row::fromValues(array_values((array)$r)),$rows));unset($rows);// 显式释放批次内存}$writer-close();$this-markDone($path);}// 生成器每次只持有一批数据 privatefunctioncursorPages():\Generator{$lastId0;do{$rowsDb::table($this-task[table])-where(id,,$lastId)-where($this-task[filters])-orderBy(id)-limit(self::BATCH)-get();if($rows-isEmpty())break;$lastId$rows-last()-id;yield$rows-all();}while($rows-count()self::BATCH);}privatefunctionmarkDone(string$path): void{Db::table(report_tasks)-where(id,$this-task[id])-update([statusdone,path$path,finished_attime()]);}}▎ 关键 Generator 确保任意时刻内存中只有 BATCH 条记录。 ▎ OpenSpout 流式追加写不缓存整个 XLSX内存恒定 ~10–15 MB/任务。 --- 三、分页查询 — 游标 vs OFFSET 对比 OFFSET 分页❌ 百万级慢 SELECT * FROM t LIMIT5000OFFSET900000→ 扫描905000行越翻越慢 游标分页✅ 恒定快 SELECT * FROM t WHEREid:last_id ORDER BYidLIMIT5000→ 走主键索引每次 O(BATCH)?php // 多列复合游标无自增ID场景 privatefunctioncursorPages():\Generator{$cursor[created_at1970-01-01,id0];do{$rowsDb::table($this-task[table])-where(function($q)use($cursor){$q-where(created_at,,$cursor[created_at])-orWhere(function($q2)use($cursor){$q2-where(created_at,$cursor[created_at])-where(id,,$cursor[id]);});})-orderBy(created_at)-orderBy(id)-limit(self::BATCH)-get();if($rows-isEmpty())break;$last$rows-last();$cursor[created_at$last-created_at,id$last-id];yield$rows-all();unset($rows);}while(true);}--- 四、Controller 入口完整闭环?php // app/Controller/ReportController.php namespace App\Controller;use Hyperf\AsyncQueue\Driver\DriverFactory;use Hyperf\HttpServer\Annotation\{Controller, Post, Get};use Hyperf\HttpServer\Contract\RequestInterface;use Hyperf\DbConnection\Db;use App\Job\ReportDispatchJob;#[Controller(prefix: /report)]class ReportController{publicfunction__construct(privatereadonlyDriverFactory$queue){}#[Post(/export)]publicfunctionexport(RequestInterface$request): array{$iduniqid(rpt_,true);Db::table(report_tasks)-insert([id$id,statuspending,paramsjson_encode($request-all()),created_attime(),]);$this-queue-get(default)-push(new ReportDispatchJob($id));return[task_id$id];}#[Get(/export/{id})]publicfunctionstatus(string$id): array{return(array)Db::table(report_tasks)-where(id,$id)-first([status,path,finished_at]);}}--- 五、三维优化效果对比 ┌────────────────┬────────────────────────────────┬──────────────────────────────────┐ │ 维度 │ 优化前 │ 优化后 │ ├────────────────┼────────────────────────────────┼──────────────────────────────────┤ │ 进程模型 │ 单进程串行阻塞 │4进程×8协程Channel背压32并发 │ ├────────────────┼────────────────────────────────┼──────────────────────────────────┤ │ 内存 │ PhpSpreadsheet 全量加载 ~800MB │ OpenSpout流式 ~12MB/任务恒定 │ ├────────────────┼────────────────────────────────┼──────────────────────────────────┤ │ 分页查询 │ OFFSET扫全表第200页耗时8s │ 游标索引每批恒定50ms │ ├────────────────┼────────────────────────────────┼──────────────────────────────────┤ │ 百万行导出耗时 │ ~15 minOOM风险 │ ~90s稳定 │ └────────────────┴────────────────────────────────┴──────────────────────────────────┘ --- 六、数据库连接池配套配置 // config/autoload/databases.phppool[min_connections16,max_connections64, // ≥4进程 ×8协程 ×2(读写)wait_timeout3.0,idle_timeout60.0,], 核心原则 游标查询保证 DB 侧 O(1)Generator 保证 PHP 侧 O(BATCH)OpenSpout 保证磁盘写入 O(1)内存三者叠加才能稳定处理百万级数据。