探讨一下大数据量的导出Excel 方案
导出的要求:
- 数据量可能最大10-20万不止
- 主表数据字段也很多
- 不能限制导出的日期和导出数量限制
- 低内存,不能把内存搞崩
目前的方案:
采用 分批次 ,一个批次5000 条, 导出成excel, 最后再把 这一批次的excel 压缩成 zip 格式提供给客户下载, 完成整体需求 扩展用的是fast-excel 压缩扩展用的是自己二次封装的:flttgo/easy-zipper 因为原作者不维护了,但是我们PHP 版本比较低,所以进行了二次处理
有好的方案欢迎留言讨论,目前不知道还有啥方案,其他语言的方案暂且不考虑,优先PHP ,其他语言对于这个问题,有同样的问题和难点,无论怎么异步,内存这块避免不掉
我的方案部分核心代码
生成批次队列的代码
$jobs = [];
$chunks = ApplyRisk::getListQuery(ApplyRiskFilter::apply($params, 'afterLoan'))
->select(['apply_risk_id'])
->chunkById(5000, function ($rows, $page) use (&$jobs) {
$ids = $rows->pluck('apply_risk_id')->toArray();
$jobs[] = new AfterLoanExportBatchJob($this->task, LazyCollection::make($ids), $page);
});
unset($chunks);
$taskId = $this->task->id;
Bus::batch($jobs)
// 所有任务完成才执行的回调
->finally(function (Batch $batch) use ($taskId) {
if ($batch->finished()) {
dispatch(new AfterLoanExportFinishJob($taskId));
}
})->dispatch();
unset($jobs);
批次队列导出格式的数据采用 yield 生成, 字段用filed 替代
public function genertateLazyData($chunks)
{
/** @var \Illuminate\Support\LazyCollection $chunk */
yield from ApplyRisk::select([
'field'
])->whereIn('apply_risk_id', $chunks)->lazy()->each(function ($row) {
yield $this->transform->afterLoan($row);
});
}
导出excel 代码
$dir = storage_path($this->task->hash_id);
if (!File::exists($dir)) {
if (! @mkdir($dir, 0755, true) && ! is_dir($dir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $dir));
}
}
$fileName = $dir.'/'.$this->task->title.'-'.$this->index.'.xlsx';
//数据准备完毕,开始导出
FastExcel::data($this->genertateLazyData($this->riskIds))->export($fileName);
上面是我这次处理的核心逻辑
本帖已被设为精华帖!
本帖由系统于 2个月前 自动加精
关于 LearnKu
如果客户不要求 csv 是低配的最好方案。数据量再大也能逐行写入,还支持追加写入。你若是使用 oss 或者 鹅厂 cos 这类对象存储的服务,都支持文件内容追加写入的 api 都不用在本地生成后再上传。
github.com/viest/php-ext-xlswriter
你可以看下这个项目,它的导入导出有固定内存模式,号称 内存仅需<1MB
如果生成时间上没有太高要求,可以用定时脚本来生成这份数据。
如果要求高一些,可以考虑使用一张冗余数据表来存储这部分数据,随着业务数据写入异步来更新冗余数据,这里可能会出现数据同步过程中的差异,需要产品和客户考虑能否接受差异以及多长时间内的差异。冗余的数据建立好查询必然要用到的索引。
再进一步,如果成本上能接受,可以考虑将冗余数据存储的方案变化为使用 es 这种搜索引擎或者 clickhouse 这种分析型数据库来加速查询。
我之前知道 xlsx 是压缩过的,今天试了下, 感觉对他再次压缩的意义不太大,如果只是想减小文件体积的话。
对了, xlsx 扩展名改成 zip 可以直接解压
看图
如果使用excel文件的话,要考虑行数溢出,单个sheet页最多支持
1048576行数据。那么根据你的要求数量溢出,只能分页导出。如果是单纯的数据导出,没有单元格设置,可以考虑csv文件。导出 csv 解决方案
创建
function.php, 在 composer 中添加自动加载,执行composer dump-autoload添加函数
创建测试控制台命令
php artisan make:command TestCommand编写测试代码
执行程序
php artisan test本机测试结果
内存占用:0.8%
CPU占用:13%-40%
CPU可能过高是因为循环遍历次数过多,占用cpu时间过长,可以适当减小
for $i, 建议在 200 - 1000,如果服务器性能高,可以适当调整。// – 补充 使用ORM查询数据
以上为测试代码,可以考究,可以纠错。
fast-excel,这里只是列出来仅供参考spoutyield生成器来做 ,比如LazyCollection按需加载queue将写入任务放入队列中,异步处理我们生成任务后后台进程执行导出到csv 100w数据导出大概6分钟
可以先csv再然后csv转excel
我们公司用的方案把导出丢在队列当中,让队列去处理,写入到CSV文件里,这样不耗内存,50W条订单数据几分钟就好了,只需要导出之后手动转换文件格式即可
总结下咱们主要的思路:
内存方面:
这块针对内存的数据一定要采用 cursor() 游标 或者 yield 迭代器去处理数据,否则大量的ORM 循环操作,内存很快都上去
查询方面:
如果数据量比如在10w 范围内,我们可以结合Laravel 自带的ORM 查询 以lazyById方式查询,一定要进行chunkId 方式查询,不要采用offset 方式。如果数据过大,楼上有建议是数据导入中间表,在处理。也或者是把数据写入ES 服务,后两种都要考虑容错
导出方式:
目前看到的方案:数据量差不多的情况如果针对格式有要求,可以用 php-ext-excel 或者 fast-excel 导出,采用迭代器方式导入数据,直接生成excel 文件。另外一个建议是数据量过大,可以先导出csv ,然后再用其他工具导出成excel ,第二个我是比较推荐的。内存占用也低,如果有composer 包支持 csv 转成 excel ,会更好
数据分块:
单个excel 过大,其实普通电脑也不好打开,综合建议是数据拆分成若干区间,写入文件,这样子体验也很好,分组之后最后压缩一个zip 文件提供下载也不错
队列使用:
使用laravel 的普通队列 + laravel 8+ 以上的Batch批次队列处理起来会更舒服
使用场景:
以上的方案,基本上能满足市面上大多数的需求
总监牛逼
定时任务异步队列导出
可以考虑用webman
xlsWrite 快的一批,就是装的时候有点麻烦
博客:laravel扩展:xlswriter导出,自定义复杂合并及样式
如果取数据方式比较复杂,涉及计算,可以考虑将数据同步OLAP库后再查数据,比如阿里云ADB,查询速度上可以省很多时间。
写文件部分我们用Go做了,支持用SQL的方式导出,写一条SQL基本就能导出,导出格式为CSV。
数据参考:单表千万级,导出CSV文件30-40W,几十秒到几分钟,一般5分钟以内。
调研过各支付平台的导出,基本都是CSV格式为主。
大量数据导出是个综合题,光用某个库不一定好解决,同时每个项目的情况不一样。
另外还需要注意下编码和数据类型格式。
不是有大佬这个吗??xlswriter-docs.viest.me/zh-cn/an-z...
github.com/mk-j/PHP_XLSXWriter 也可以参考下这个方法,复制或者引入类都可以,比较轻量,也很好用。实际业务中通过job执行,数据通过clickhouse查询的, 50多万行写入excel 并上传到阿里云速度也很快的
正好可以推广下自己的博文了 :-)
dbwu.tech/posts/optimize/big_data_...