核心选型: 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->semaphore=new 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();// 释放槽}});}}}▎ 策略:nums=4(进程数=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 BATCH=5000;// 每批行数,可调 publicfunction__construct(privatereadonlyarray$task){}publicfunctionrun(): void{$path=sprintf('/tmp/reports/%s.xlsx',$this->task['id']);$options=new Options();$options->DEFAULT_ROW_STYLE=(new Style())->setShouldWrapText(false);$writer=new 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{$lastId=0;do{$rows=Db::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(['status'=>'done','path'=>$path,'finished_at'=>time()]);}}▎ 关键: 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_at'=>'1970-01-01','id'=>0];do{$rows=Db::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{$id=uniqid('rpt_',true);Db::table('report_tasks')->insert(['id'=>$id,'status'=>'pending','params'=>json_encode($request->all()),'created_at'=>time(),]);$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 min(OOM风险) │ ~90s(稳定) │ └────────────────┴────────────────────────────────┴──────────────────────────────────┘ --- 六、数据库连接池配套配置 // config/autoload/databases.php'pool'=>['min_connections'=>16,'max_connections'=>64, // ≥4进程 ×8协程 ×2(读写)'wait_timeout'=>3.0,'idle_timeout'=>60.0,], 核心原则: 游标查询保证 DB 侧 O(1),Generator 保证 PHP 侧 O(BATCH),OpenSpout 保证磁盘写入 O(1)内存,三者叠加才能稳定处理百万级数据。