thinkphp6 + redis实现大数据导出excel超时或内存溢出问题解决方案

redis下载安装(window版本)
参考地址:https://blog.csdn.net/Ci1693840306/article/details/144214215

php安装redis扩展
参考链接:https://blog.csdn.net/jianchenn/article/details/106144313

解决思路:(分批处理,最后合并)
业务逻辑:本项目由于涉及到多张数据表,导出业务逻辑为:先查询主表,查询出数据后通过foreach遍历数据,并在遍历循环中根据与主表关联的字段查询另外几张表对应数据。
解决方案:
后端:

  1. 先将字典表、需要在循环中查询的所有数据表存储到redis中(如果数据过多,可以将其分为多个方法)
  2. 将导出数据接口中的数据进行分页,根据页码数导出相应的数据条数,并存放至临时excel文件中。等待全部执行完毕后将这些临时文件合并成一个excel文件,并返回。

前端:

  1. 首先请求字典等数据表存入redis的接口
  2. 请求导出数据接口,每次传入当前需导出数据的页码数,在没有全部完成之前页数++,直到完成后执行下载文件操作

PHP-Xlswriter扩展安装:
官网:https://xlswriter-docs.viest.me/zh-cn/an-zhuang/windows
在这里插入图片描述

在thinkphp6项目中打开config/cache.php,加上redis配置参数:

<?php

// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------

return [
    // 默认缓存驱动
    'default' => env('cache.driver', 'file'),

    // 缓存连接方式配置
    'stores'  => [
        'file' => [
            // 驱动方式
            'type'       => 'File',
            // 缓存保存目录
            'path'       => '',
            // 缓存前缀
            'prefix'     => '',
            // 缓存有效期 0表示永久缓存
            'expire'     => 0,
            // 缓存标签前缀
            'tag_prefix' => 'tag:',
            // 序列化机制 例如 ['serialize', 'unserialize']
            'serialize'  => [],
        ],
        // 更多的缓存连接
        'redis' => [
            'type'   => 'redis',
            // 缓存主机
            'host'       => '127.0.0.1',
            // 缓存端口
            'port'     => '6379',
            // 缓存密码
            'password'     => '',
            // 缓存数据库
            'select'   => 0,
            // 缓存有效期 0表示永久缓存
            'timeout'   => 0,
            // 缓存前缀
            'prefix'   => ''
        ]
    ],
    
];

后端路由route/app.php

Route::post('export_data/:code/:id', 'Basicinfo/export_data'); // 根据指定条件和字段导出数据
Route::post('export_save_redis/:code/:id', 'Basicinfo/export_save_redis'); // 导出之前将部分数据存入redis
Route::post('export_save_redis2/:code/:id', 'Basicinfo/export_save_redis2'); // 导出之前将部分数据存入redis

控制器 BasicinfoController.php

<?php
namespace app\controller;
use app\service\BasicinfoService;
class BasicinfoController {
    public function export_save_redis(){
        $data = input('post.');
        $service = new BasicinfoService;
        $res = $service->export_basicinfo_save_redis($data);
        return show(true, '', $res);
    }
    public function export_save_redis2(){
        $data = input('post.');
        $service = new BasicinfoService;
        $res = $service->export_basicinfo_save_redis_person($data);
        return show(true, '', $res);
    }
    public function export_data(){
        $data = input('post.');
        $service = new BasicinfoService;
        $res = $service->export_data($data);
        if($res['state']){
            if(file_exists($res['file'])){
                return show(true, '', $res['file']);
            }else{
                return show(false, '文件导出失败');
            }
        }else{
            return show(false, '', $res);
        }
    }
}

BasicinfoService.php

<?php
namespace app\service;

// 引入其他文件...

class BasicinfoService{
    public function export_save_redis($data){
        $mapper = new BasicinfoMapper;
        $mapper -> export_save_redis($data['search']??[]);
        return true;
    }
    // 由于第此数据表数据量过多,导致保存失败,单独处理
    public function export_save_redis2($data){
        $mapper = new BasicinfoMapper;
        $mapper -> export_save_redis2($data['search']??[]);
        return true;
    }
    public function export_data($data){

        $result_data = [];
        $data['page'] = isset($data['page']) ? $data['page'] : 1;
       
        // 获取所有要查询的字段和名称
        $header_arr = [];
        $fields_arr = [];
        foreach($data['export_data'] as $key=>$val){
            array_push($header_arr, $val['label']);
            array_push($fields_arr, $val['field']);
        }
        // 文件存储目录
        $public = app()->getRootPath().'public/';
        $path = 'uploads/export_data/';

        $file_name = !empty($data['file_name']) ? $data['file_name'] : 'basicinfo_'.rand(99999, 99999999);
        $result_data['file_name'] = $file_name;
        $fileName = $file_name.'_'.$data['page'].'.xlsx';
        if(!file_exists($path)){
            mkdir($path, 0777);
        }
        $excel_config = [
            'path' => $public.$path // xlsx文件保存路径
        ];
        $excel  = new \Vtiful\Kernel\Excel($excel_config);
        $fileObject = $excel->fileName($fileName, 'sheet1');
        
        $mapper = new BasicinfoMapper;
        $page_size = 5000; // 每次查询的数据条数
        // 在第1页时需要查询总数量,并获取总页数
        if($data['page'] == 1){
            // 获取总数
            $count = $mapper->get_count_basicinfo($data['search']??[]);
            $loop_num = ceil($count/$page_size);
            $data['total_page'] = $loop_num; // 设置总页数
            $result_data['total_page'] = $loop_num; // 返回总页数
        }else{
            $result_data['total_page'] = $data['total_page'];
        }
        $loop_page = 2; // 每页执行n次循环
        $loop_num = $loop_page;
        // 检测当前页数 * 循环数量 >= 总页数后
        if(intval($data['page'] * $loop_page) >= intval($data['total_page'])){
            if(intval($data['page'] * $loop_page) == intval($data['total_page'])){
                $loop_num = 1;
            }else{
                $loop_num = ($data['page'] * $loop_num) - $data['total_page'];
            }
        }
        // 当前循环完成后最大id
        $max_id = !empty($data['max_id']) ? $data['max_id'] : 0;
        for($l=0; $l<$loop_num; $l++){
            $list = $mapper->export_list_basicinfo($data['search']??[], $fields_arr, $max_id, $page_size);
            if($list){
                $max_id = $list[count($list)-1]['organ_id']; // 重置最大id
                // var_dump($max_id);
                $content = [];
                $i=0;
                foreach($list as &$val){
                    
                    // 处理转化数据值
                    // ...
                    $result = [];
                    foreach ($fields_arr as $key) {
                        if (isset($val_arr[$key])) {
                            $result[$key] = $val[$key];
                        }else{
                            $result[$key] = '';
                        }
                    }
                    $content[$i] = array_values($result);
                    $i++;
                    unset($val_arr);
                    unset($val);
                    unset($result);
                }
                // 将数据写入xls文件
                if(count($content) > 0){
                    $fileObject->data($content)->output();
                    unset($content);
                }
                
            }  
            unset($list);
        }
        $result_data['max_id'] = $max_id;
        // 检测如果为最后一页,则将文件合并后返回
        if(intval($data['page'] * $loop_page) >= intval($data['total_page'])){
            $result_data['state'] = true;
            // 处理合并文件
            $res = $this->merge_export_file($file_name, $data['page'], $header_arr);
            // $file_dir = 'uploads/export_data/'.$fileName;
            $result_data['file'] = $res;
        }else{
            $result_data['state'] = false;
            $result_data['file'] = '';
        }
        
        return $result_data;
    }
    // 处理合并文件
    public function merge_export_file($fileName, $page, $header_arr){
        $public = app()->getRootPath().'public/';
        $path = 'uploads/export_data/';
        $excel_config = [
            'path' => $public.$path // xlsx文件保存路径
        ];
        $excel  = new \Vtiful\Kernel\Excel($excel_config);
        $res_file = $fileName . '_all.xlsx';
        $fileObject = $excel->fileName($res_file, 'sheet1');
        // // 设置样式
        $fileHandle = $fileObject->getHandle();
        $format = new \Vtiful\Kernel\Format($fileHandle);
        $alignStyle = $format
                        ->align(\Vtiful\Kernel\Format::FORMAT_ALIGN_VERTICAL_CENTER, \Vtiful\Kernel\Format::FORMAT_ALIGN_CENTER_ACROSS)
                        ->toResource();
        $boldStyle = $format
                        // ->bold() // 加粗
                        // ->wrap() // 文本换行
                        // ->background(0xFFB6C1) // 设置背景颜色 颜色常量和16进制数
                        ->align(\Vtiful\Kernel\Format::FORMAT_ALIGN_CENTER, \Vtiful\Kernel\Format::FORMAT_ALIGN_VERTICAL_CENTER) // 文本居中
                        ->toResource();
        // // fileName 会自动创建一个工作表,你可以自定义该工作表名称,工作表名称为可选参数
        $fileObject->header($header_arr)
        // ->data($content)
        ->defaultFormat($alignStyle)
        ->setRow('A1', 30, $boldStyle)
        ->setRow('A2:A9999', 20) // 行高
        // ->setColumn('A:A', '10')
        ->setColumn('A:CZ', 30)
        ->output();


        $file_arr = [];
        for($i=1; $i<=$page; $i++){
            $nowFile = $fileName.'_'.$i.'.xlsx';
            array_push($file_arr, $nowFile);
            if(file_exists($path.$nowFile)){
                $data = $excel->openFile($nowFile)
                    ->openSheet()
                    ->getSheetData();
                $fileObject->data($data)->output();
            }
        }
        // 回收资源
        $fileObject -> close();
        // 删除临时文件
        foreach($file_arr as $file){
            if(file_exists($path.$file)){
                unlink($path.$file);
            }
        }
        return $path.$res_file;

    }
}

BasicinfoMapper.php

<?php
namespace app\mapper;
use think\facade\Db;
use app\model\Basicinfo;
use think\facade\Cache;
class BasicinfoMapper
{
    public function export_save_redis($search){
        $where = '';
        
        $out_time = 600; // 缓存时间秒
        $batch_size = 2000;  // 每批次处理的数量
        $redis = Cache::store('redis')->handler();
        $redis->flushDb(); // 清空redis缓存

        $sql1= 'select main_id,field1,field2,... from table1  where 1=1 '.$where;
        $list1 = Db::query($sql1);
        if(!empty($list1)){
            // 建立管道
            $pipe1 = $redis->pipeline();
            $batches = array_chunk($list1, $batch_size); // 将数据分批
            foreach ($batches as $batch) {
                foreach ($batch as $v) {
                    $pipe1->hMSet('table1_'.$v['main_id'], $v);
                    $pipe1->expire('table1_'.$v['main_id'], $out_time); // 设置过期时间
                }
                $pipe1->exec(); // 执行当前批次
                $pipe1 = $redis->pipeline(); // 重新初始化管道
            }
        }
        unset($list1);
        $sql2= 'select main_id,field1,field2,... from table2  where 1=1 '.$where;
        $list2 = Db::query($sql2);
        if(!empty($list2)){
            $pipe2 = $redis->pipeline();
            $batches2 = array_chunk($list2, $batch_size); // 将数据分批
            foreach ($batches2 as $batch) {
                foreach ($batch as $v) {
                    $pipe2->hMSet('table2_'.$v['main_id'], $v);
                    $pipe2->expire('table2_'.$v['main_id'], $out_time); // 设置过期时间
                }
                $pipe2->exec(); // 执行当前批次
                $pipe2 = $redis->pipeline(); // 重新初始化管道
            }
        }
        unset($list2);
    }
    public function export_save_redis2($search){
        $where = '';
       
        $out_time = 600; // 缓存时间秒
        $batch_size = 2000;  // 每批次处理的数量
        $redis = Cache::store('redis')->handler();
        // $redis->flushDb(); // 清空redis缓存
        // $temp = '';

        $sql = 'select main_id,field1,field2,... from table3 where type in (1,3) '.$where;
        $list = Db::query($sql);
        if(!empty($list)){
            $pipe = $redis->pipeline();
            $batches = array_chunk($list, $batch_size); // 将数据分批
            foreach ($batches as $batch) {
                foreach ($batch as $v) {
                    $key = $v['type'] == 1 ? 'table3_1_'.$v['main_id'] : 'table3_3_'.$v['main_id'];
                    $pipe->hMSet($key, $v);
                    $pipe->expire($key, $out_time);
                }
                $pipe->exec(); // 执行当前批次
                $pipe = $redis->pipeline(); // 重新初始化管道
            }
        }
        unset($list);
    }
    public function export_list_basicinfo($search, $fields=[], $max_id=0, $limit=1000){
    $pFields = [];
    $dFields = [];
    $cFields = [];
    $fields = array_reduce($fields, function($carry, $field) use (&$pFields,&$dFields,&$cFields) {
        if (substr($field, 0, 6) === 'table1_') {
            $dFields[] = $field;
        } else if (substr($field, 0, 7) === 'table2_') {
        // $carry[] = 'n.' . $field;
            $cFields[] = $field;
        } else if (substr($field, 0, 6) === 'table3') {
            $pFields[] = $field;
        } else {
            $carry[] =  $field;
        }
        return $carry;
    }, []);

    $fields = implode(',', $fields);
    $where = '1 = 1';
    // 其他条件...
    
    $sql = 'select main_id,'.$fields." from basicinfo   where ".$where.' and main_id > '.$max_id.' order by main_id limit '.$limit;
    $redis = Cache::store('redis')->handler();
    $list = Db::query($sql);
    if(!$list){ return []; }

    foreach ($list as $k => $v){
        
        $fd = [];
        if(count($dFields) > 0){
            $p_info = $redis->hGetAll('table1_'. $v['main_id']);
            if(!empty($p_info)){
                $fd = $p_info;
            }
        }

        if(in_array('organ', $dFields)){
            $list[$k]['organ'] = !empty($fd) ? $fd['organ']:'';
        }

        $fc = [];
        if(count($cFields) > 0){
            if (!empty($redis->hGetAll('table2_'. $v['main_id']))) {
                $fc = $redis->hGetAll('table2_'. $v['main_id']);
            }
        }
        if(in_array('party', $cFields)){
            $list[$k]['party'] = !empty($fc) ? $fc['party'] : 0;
        }
        $jbr = [];
        $fr = [];
        if(count($pFields) > 0){
            $table31_info = $redis->hGetAll('table3_1_'. $v['organ_idno']);
            $table33_info = $redis->hGetAll('table3_3_'. $v['organ_idno']);
            if(!empty($table31_info)){
                $jbr = $table31_info;
            }
            if(!empty($table33_info)){
                $fr = $table33_info;
            }
        }

        if(in_array('name', $pFields)){
            $list[$k]['name'] = !empty($jbr) ? $jbr['name']:'';
        }
    }
    return $list;
}
}

前端:

<template>
	<el-button type="primary" style="margin-top: 30px;" @click="clickExportData">导出</el-button>
</template>
<script>
import downloadFile from '@/plugins/downloadFile';
export default {
    data() {
        return {
            percentage:0, //进度条的占比
            progressShow: false, // 是否显示进度条弹出层
            dowPage: 1
        }
    },
    methods: {
        clickExportData(){
            const data = {};
            // 获取被选中的字段
            let result = [];
            data['export_data'] = result;
            data['search'] = {};

            this.progressShow = true;
            this.percentage = 0;

            this.$http.post("export_save_redis/"+this.code+'/'+this.user_id, data).then(res1 => {
                // console.log(res1);
                this.$http.post("export_save_redis2/"+this.code+'/'+this.user_id, data).then(res2 => {
                    this.exportData(data, times);
                })
            });
        },
        // 导出数据
        async exportData(data, times){
            data.page = this.dowPage;
            let url = "export_data/"+this.code+'/'+this.user_id;
            const {data:res} = await this.$http.post(url, data);
            console.log(res);
            if(res.state){
                console.log('进行文件下载')
                var xls_url = this.$request_url + res.content;
                clearInterval(times); // 清除计时器
                // 进行下载文件操作
                // ...
                // downloadFile.getProgress(xls_url, '下载文件_'+new Date().getTime(), this, this.percentage);
            }else{
                if(res.content){
                    this.dowPage ++;
                    data.file_name = res.content.file_name;
                    data.total_page = res.content.total_page;
                    data.max_id = res.content.max_id;
                    this.exportData(data, times);
                }else{
                    this.progressShow = false;
                    clearInterval(times);
                    this.$message.error('数据导出失败,请稍后重试!');
                }
            }
        },
    }
}
</script>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/955121.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HTML中最基本的东西

本文内容的标签&#xff0c;将是看懂HTML的最基本之基本 &#xff0c;是跟您在写文章时候一样内容。一般想掌握极其容易&#xff0c;但是也要懂得如何使用&#xff0c;过目不忘&#xff0c;为手熟尔。才是我们学习的最终目的。其实边看边敲都行&#xff0c;或者是边看边复制粘贴…

【大前端】Vue3 工程化项目使用详解

目录 一、前言 二、前置准备 2.1 环境准备 2.1.1 create-vue功能 2.1.2 nodejs环境 2.1.3 配置nodejs的环境变量 2.1.4 更换安装包的源 三、工程化项目创建与启动过程 3.1 创建工程化项目 3.2 项目初始化 3.3 项目启动 3.4 核心文件说明 四、VUE两种不同的API风格 …

-bash: /java: cannot execute binary file

在linux安装jdk报错 -bash: /java: cannot execute binary file 原因是jdk安装包和linux的不一致 程序员的面试宝典&#xff0c;一个免费的刷题平台

业务幂等性技术架构体系之消息幂等深入剖析

在系统中当使用消息队列时&#xff0c;无论做哪种技术选型&#xff0c;有很多问题是无论如何也不能忽视的&#xff0c;如&#xff1a;消息必达、消息幂等等。本文以典型的RabbitMQ为例&#xff0c;讲解如何保证消息幂等的可实施解决方案&#xff0c;其他MQ选型均可参考。 一、…

RustDesk ID更新脚本

RustDesk ID更新脚本 此PowerShell脚本自动更新RustDesk ID和密码&#xff0c;并将信息安全地存储在Bitwarden中。 特点 使用以下选项更新RustDesk ID&#xff1a; 使用系统主机名生成一个随机的9位数输入自定义值 为RustDesk生成新的随机密码将RustDesk ID和密码安全地存储…

QT开发技术 【基于TinyXml2的对类进行序列化和反序列化】一

一、对TinyXml2 进行封装 使用宏 实现序列化和反序列化 思路&#xff1a; 利用宏增加一个类函数&#xff0c;使用序列化器调用函数进行序列化 封装宏示例 #define XML_SERIALIZER_BEGIN(ClassName) \ public: \virtual void ToXml(XMLElement* parentElem, bool bSerialize …

金仓Kingbase客户端KStudio报OOM:Java heap space socketTimeout

找到Kingbase\ES\V8\KESRealPro\V008R006C006B0021\ClientTools\guitools\KStudio\KStudio.ini 修改JVM参数&#xff1a; 默认值&#xff1a; -Xms512m -Xmx1024m 改为&#xff1a; -Xms1024m -Xmx2048m -XX:MaxPermSize512m SQL查询报错&#xff1a;An I/O error occurred …

Redis--21--大Key问题解决方案

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言Redis--20--大Key问题解析 一、如何发现Redis大Key1. 使用Redis命令行工具**MEMORY USAGE****RANDOMKEY****DEBUG OBJECT****SCAN命令****redis-cli 工具&#…

【进程与线程】进程的状态

在操作系统中&#xff0c;进程是执行中的程序实例。进程在其生命周期中会经历不同的状态&#xff0c;操作系统根据进程的执行情况和资源调度&#xff0c;将进程划分为多个状态。 这些状态帮助操作系统更加高效地管理 CPU 和系统资源。 进程的状态&#xff1a;就绪态&#xff0…

K8S开启/关闭审计日志

K8S默认禁用审计 开启/关闭 k8s 审计日志 默认 Kubernetes 集群不会输出审计日志信息。通过以下配置&#xff0c;可以开启 Kubernetes 的审计日志功能。 准备审计日志的 Policy 文件配置 API 服务器&#xff0c;开启审计日志重启并验证 准备审计日志 Policy 文件 apiVersio…

nginx: [emerg] bind() to 0.0.0.0:80 failed 端口被占用

nginx: [emerg] bind() to 0.0.0.0:80 failed (10013: An attempt was made to access a socket in a way forbidden by its access permissions) 查看被占用的端口 被系统占用了 HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/HTTP 然后再进入nginx文件夹目录下…

vue2 web 多标签输入框 elinput是否当前焦点

又来分享一点点工作积累及解决方案 产品中需要用户输入一些文字后按下回车键生成标签来显示在页面上&#xff0c;经过尝试与改造完成如下&#xff1a; <template><div class"tags-view" click"beginInput"><el-tag :key"index" …

vLLM私有化部署大语言模型LLM

目录 一、vLLM介绍 二、安装vLLM 1、安装环境 2、安装步骤 三、运行vLLM 1、运行方式 2、切换模型下载源 3、运行本地已下载模型 四、通过http访问vLLM 一、vLLM介绍 vLLM&#xff08;官方网址&#xff1a;https://www.vllm.ai&#xff09;是一种用于大规模语言模型&#x…

STM32的集成开发环境STM32CubeIDE安装

STM32CubeIDE - STM32的集成开发环境 - 意法半导体STMicroelectronics

硬件学习笔记--31 IEC62053-21相关内容介绍

IEC 62053-21是一项由国际电工委员会&#xff08;International Electrotechnical Commission&#xff0c;简称IEC&#xff09;发布的国际标准&#xff0c;全称为《交流电能测量设备&#xff08;a.c.&#xff09;-特殊要求-第21部分&#xff1a;静止式有功能量计量表&#xff0…

C++类与对象(一)—学习记录

序言&#xff1a;要想开发一款成功的应用程序&#xff0c;其开发者必须充分了解并实现用户的需求。作为一个设计良好的类&#xff0c;既要有直观且易于使用的接口&#xff0c;也必须具备高效的实现过程。 一、类与对象基本概念 面向对象程序设计的主要特点为抽象、封装、继承与…

七大排序算法

文章目录 排序的概念及引用1.插入排序2.希尔排序(缩小增量排序)3.选择排序4.堆排序5.冒泡排序6.快速排序7.归并排序8.代码排序部分的测试9.代码加效果大致测试时间&#xff08;仅供参考&#xff09; 排序的概念及引用 排序:将数据按照特定的规律排成递增或递减的操作 稳定性:…

Windows 蓝牙驱动开发-蓝牙设备栈

蓝牙设备栈 蓝牙驱动程序堆栈包含 Microsoft 为蓝牙协议提供支持的核心部分。 有了这个堆栈&#xff0c;已启用蓝牙的设备可以彼此定位并建立连接。 在此类连接中&#xff0c;设备可以通过各种应用程序交换数据并彼此交互。 下图显示了蓝牙驱动程序堆栈中的模块&#xff0c;以…

C# 获取PDF文档中的字体信息(字体名、大小、颜色、样式等

在设计和出版行业中&#xff0c;字体的选择和使用对最终作品的质量有着重要影响。然而&#xff0c;有时我们可能会遇到包含未知字体的PDF文件&#xff0c;这使得我们无法准确地复制或修改文档。获取PDF中的字体信息可以解决这个问题&#xff0c;让我们能够更好地处理这些文件。…

第十三章:数据库技术

文章目录&#xff1a; 一&#xff1a;基础 1.概念 2.特点 3.常见数据库品牌 4.数据库应⽤系统 4.1 C/S 4.2 B/S 5.数据模型的分类 6.名词解析 7.关系运算 二&#xff1a;Access 1.基础 2.操作 2.1 建立表 2.2 维护表 2.3 创建查询 2.4 创建窗体 2.5 创建报表…