最近可能会遇到大量数据导出Excel的场景,今天趁现在需求告一段落来做下技术预研,然后这里就顺便分享给大家。

一、数据量预判

因为我们是做物联网的,这里要导出的数据就是设备的上报数据。客户说要这些数据导出成excel进行分析,又或是其他什么原因,咱不管。咱就分析下数据量,目前设备数量1500,2小时上报一次数据(最小可设置为半小时),要求可以导出3年的数据。
数据量初步估算:1500 * 12 * 30 * 12 * 3 = 19,440,000 ,设备而且据说还要上,所以估计数据量就是千万级吧。
现在Excel2007以后单个sheet能放的数据是1,048,576 ,列肯定是够用的,sheet数量以前是255,现在肯定也是够用的。但是唯一一点就是这个excel文件能不能打开还跟电脑的内存有关,这个肯定要跟客户说清楚。了解这些后就可以开始先跟客户掰扯下,告知风险,看能不能干掉这样的需求,又或是提供一些方案。

二、方案设计

之前小数据量是接口返回数据给前端,前端生成excel文件。现在这么大的数据量,估计要后端实现了。后端也不能一次性把数据查询出来,内存也吃不消,搞不好就报oom错误了。所以批量查询时肯定的。方案设计如下:
1、批量查询写入数据到excel,控制单个sheet的数据量,即将超过就新建sheet继续写入
2、在1的基础上批量查询改为流式查询(应该流式查询性能会高,数据库的连接次数、查询用的缓存应该是下降的)
3、在2的基础上分多个excel文件(理论跟实际还是有差异,理论上数据量没有问题,实际上怕客户电脑内存不够打开不了)
4、在3的基础上引入自动任务,定期生成历史数据excel文件
5、在3、4的基础上,合并多个excel文件为zip包,给客户下载
6、特殊方案,前端与后端建立ws连接,后端接口流式查询将结果发布到ws,前端消费ws里的数据,分sheet写入,分excel写入。

三、流式查询

单表是基于tk.mybatis,上关键代码跟测试示例吧:

extend就是基于tk.mybatis的实现的单表流式查询。
StreamExampleProvider


import org.apache.ibatis.mapping.MappedStatement;
import tk.mybatis.mapper.mapperhelper.MapperHelper;
import tk.mybatis.mapper.provider.ExampleProvider;/*** @author zhengwen**/
public class StreamExampleProvider extends ExampleProvider {/**** @param mapperClass* @param mapperHelper*/public StreamExampleProvider(Class<?> mapperClass, MapperHelper mapperHelper) {super(mapperClass, mapperHelper);}/*** 根据Example流式查询** @param ms* @return*/public String selectStreamByExampleMapper(MappedStatement ms) {return this.selectByExample(ms);}/*** 根据Example和RowBounds流式查询** @param ms* @return*/public String selectStreamByExampleRowBoundsMapper(MappedStatement ms) {return this.selectByExample(ms);}}

SelectStreamByExampleMapper


import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.session.ResultHandler;
import tk.mybatis.mapper.annotation.RegisterMapper;/*** @author zhengwen**/
@RegisterMapper
public interface SelectStreamByExampleMapper<T> {/*** 根据example条件和RowBounds进行流式查询** @param example* @param resultHandler*/@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)@SelectProvider(type = StreamExampleProvider.class, method = "dynamicSQL")void selectStreamByExampleMapper(Object example, ResultHandler resultHandler);
}

SelectStreamByExampleRowBoundsMapper


import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.mapping.ResultSetType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import tk.mybatis.mapper.annotation.RegisterMapper;/*** @author zhengwen**/
@RegisterMapper
public interface SelectStreamByExampleRowBoundsMapper<T> {/*** 根据example条件和RowBounds进行流式查询** @param example* @param rowBounds* @param resultHandler*/@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)@SelectProvider(type = StreamExampleProvider.class, method = "dynamicSQL")void selectStreamByExampleRowBoundsMapper(Object example, RowBounds rowBounds, ResultHandler resultHandler);}

StreamMapper


import tk.mybatis.mapper.annotation.RegisterMapper;/*** @author zhengwen**/
@RegisterMapper
public interface StreamMapper<T> extendsSelectStreamByExampleMapper<T>,SelectStreamByExampleRowBoundsMapper<T> {}

我们的mapper实现 StreamMapper


import com.easylinkin.bm.core.extend.StreamMapper;
import tk.mybatis.mapper.common.BaseMapper;
import tk.mybatis.mapper.common.ConditionMapper;
import tk.mybatis.mapper.common.IdsMapper;
import tk.mybatis.mapper.common.MySqlMapper;
import tk.mybatis.mapper.common.special.InsertListMapper;/*** 定制版MyBatis Mapper插件接口,如需其他接口参考官方文档自行添加。** @author zhengwen*/
public interface Mapper<T>extendsBaseMapper<T>,ConditionMapper<T>,IdsMapper<T>,InsertListMapper<T>,MySqlMapper<T>,StreamMapper<T> {}

这样,我们的业务mapper:


/*** @author unknown*/
public interface DeviceSensirionRecordMapper extends Mapper<DeviceSensirionRecord> {/*** 分组排序list数据** @param deviceSensirionRecordPageVo 页面入参vo* @return 上报数据分组排序*/List<DeviceSensirionRecordDto> listGroupAndSort(DeviceSensirionRecordPageVo deviceSensirionRecordPageVo);/*** 分组排序list数据流式查询** @param deviceSensirionRecordPageVo 页面入参vo* @return 上报数据分组排序数据游标cursor*/Cursor<DeviceSensirionRecordDto> listGroupAndSortStream(DeviceSensirionRecordPageVo deviceSensirionRecordPageVo);
}

四、流式查询使用

我的junit测试类演示了3种使用方式:@Testpublic void mapperStreamQueryTest() {log.info("---流式查询测试--");//组织模拟查询条件对象DeviceSensirionRecordPageVo deviceSensirionRecordPageVo = new DeviceSensirionRecordPageVo();log.info("---{}", JSONObject.toJSONString(deviceSensirionRecordPageVo));DeviceSensirionRecord exp = deviceSensirionRecordPageVo.getDeviceSensirionRecord();Condition condition = new Condition(DeviceSensirionRecord.class);Example.Criteria cri = condition.createCriteria();cri.andEqualTo("deviceNo", exp.getDeviceNo());//基于我们刚刚扩展tk.mybatis单表流式查询deviceSensirionRecordMapper.selectStreamByExampleMapper(condition, resultHandler -> {DeviceSensirionRecord rs = (DeviceSensirionRecord) resultHandler.getResultObject();log.info("----{}", JSONObject.toJSONString(rs));});//基于游标Cursor<DeviceSensirionRecordDto> cs = deviceSensirionRecordMapper.listGroupAndSortStream(deviceSensirionRecordPageVo);cs.forEach(c->{log.info("----Cursor-------{}", JSONObject.toJSONString(c));});//基于sqlSessionTemplatesqlSessionTemplate.select("com.xx.xx.dao.DeviceSensirionRecordMapper.listGroupAndSort",deviceSensirionRecordPageVo,new RowBounds(1,1000) , rs->{DeviceSensirionRecordDto ds = (DeviceSensirionRecordDto) rs.getResultObject();log.info("----{}", JSONObject.toJSONString(ds));});}

里面有3种方式:
1、基于我们刚刚扩展tk.mybatis单表流式查询
2、基于游标,mybatis支持的
3、基于sqlSessionTemplate
本来还准备扩展下tk.mybatis的流式查询支持复杂查询的,但是试了2、3后决定放弃了,直接用也很香很简单,没有意义在扩展。

五、流式查询配合多sheet生成excel

方案应该就上面那些,今天先研究到的是流式查询配合多sheet生成。我这里流式查询研究到了单表的、自定义的。这里直接上复杂的流式查询配合多sheet生成excel,看我的junit测试类:
@Testpublic void streamQueryExcel() {log.info("---流式查询生成Excel--");String name = DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss");String fileName = baseTempPath + File.separator + name + ".xlsx";File exFile = new File(fileName);ExcelWriter excelWriter = ExcelUtil.getBigWriter(exFile, "上报数据-1");List<Map<String, String>> ls = new ArrayList<>();//sheet的index、数据序号初始AtomicInteger sheetIndx = new AtomicInteger(0);AtomicInteger index = new AtomicInteger(0);DeviceSensirionRecordPageVo deviceSensirionRecordPageVo = new DeviceSensirionRecordPageVo();//mybatis流式查询SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession();Cursor<DeviceSensirionRecordDto> cs = sqlSession.getMapper(DeviceSensirionRecordMapper.class).listGroupAndSortStream(deviceSensirionRecordPageVo);cs.forEach(c -> {log.info("----Cursor-------{}", JSONObject.toJSONString(c));//数据转换converToExcelData(index, c, ls);//达到量就批量写入if (index.get() == 10000) {writeExcelData(sheetIndx, excelWriter, ls);//序号、数据对象清空ls.clear();index.set(0);}});//最后一批数据写入excelif (CollectionUtil.isNotEmpty(ls)) {writeExcelData(sheetIndx, excelWriter, ls);}//最后刷新文件excelWriter.flush(exFile);excelWriter.close();log.info("---------end------------");}/*** 数据转换** @param index* @param dto* @param ls*/private void converToExcelData(AtomicInteger index, DeviceSensirionRecordDto dto, List<Map<String, String>> ls) {index.set(index.get() + 1);Map<String, String> mp = new HashMap<>();mp.put("index", String.valueOf(index.get()));mp.put("deviceNo", dto.getDeviceNo());mp.put("temperature", String.valueOf(dto.getTemperature()));mp.put("humidity", String.valueOf(dto.getHumidity()));mp.put("power", String.valueOf(dto.getPower()));mp.put("recordTime", DateUtil.format(dto.getRecordTime(), "yyyy-MM-dd HH:mm:ss"));ls.add(mp);}/*** 批量写数据到excel** @param sheetIndx* @param excelWriter* @param ls*/private void writeExcelData(AtomicInteger sheetIndx, ExcelWriter excelWriter, List<Map<String, String>> ls) {sheetIndx.set(sheetIndx.get() + 1);if (sheetIndx.get() != 0) {//设置sheet名称excelWriter.setSheet("上报数据-" + sheetIndx);}//列头writeExcelHead(excelWriter);//数据写入excelWriter.write(ls, true);excelWriter.autoSizeColumnAll();}/*** 列头设置* @param excelWriter*/private void writeExcelHead(ExcelWriter excelWriter) {excelWriter.addHeaderAlias("index", "序号");excelWriter.addHeaderAlias("deviceNo", "设备编码");excelWriter.addHeaderAlias("temperature", "温度");excelWriter.addHeaderAlias("humidity", "湿度");excelWriter.addHeaderAlias("power", "电量");excelWriter.addHeaderAlias("recordTime", "上报时间");}

我这里是使用的HuTool的工具包,不得不说,真香啊。以前也用easyExcel、myExcel,或者直接用poi,但是都对代码有一定的侵入性,我抛弃了。

六、流式查询配合多sheet生成excel效果


最后说一点,流式查询应该是长时间需要占数据库连接的,所以也需要谨慎使用。
就先分享到这里,希望可以启发大家。特殊方案要前端配合,没有下载环节,但是写数据到sheet、生成多个excel这些逻辑都是前端写了,估计一般的前端都不会愿意。他们的理论是他们是使用数据,哈哈。其实吧我觉得这也是使用数据啊,不纠结,我们后端啥都可以干,没必要别人不愿意还非要,那就有点QJ了。

大量数据导出Excel方案相关推荐

  1. GridView的操作:导出Excel[方案一]

    GridView的操作:从GridView导出数据到Excel 方案一:简单可行的方法  1protected void WriteExcel_Btn_Click(object sender, Eve ...

  2. 将页面中表格数据导出excel格式的文件(vue)

    近期由于项目需要,需要将页面中的表格数据导出excel格式的文件,折腾了许久,在网上各种百度,虽然资料不少,但是大都不全,踩了许多坑,总算是皇天不负有心人,最后圆满解决了. 1.安装相关依赖(npm安 ...

  3. Java程序员从笨鸟到菜鸟之(一百零四)java操作office和pdf文件(二)利用POI实现数据导出excel报表...

    在上一篇博客中,我们简单介绍了java读取word,excel和pdf文档内容 ,但在实际开发中,我们用到最多的是把数据库中数据导出excel报表形式.不仅仅简单的读取office中的数据.尤其是在生 ...

  4. Controller数据导出Excel 详细教程——easypoi-base,easypoi-web,easypoi-annotation

    Controller获取数据导出Excel,详细教程 1:导入对应依赖 <dependency><groupId>cn.afterturn</groupId>< ...

  5. DataTable中的数据导出Excel文件

    DataTable中的数据导出Excel文件 View Code ///<summary> /// 将DataTable中的数据导出到指定的Excel文件中 ///</summary ...

  6. php数据的导出到excel,php 数据的导出到excel表格-怎么将php数据导出excel

    怎么使用php把表格中的数据导入到excel中 下面写的一个PHP导出数据到CSV问价的函数,你到时候直接调用了 /** * 导出CSV * @param string $fileName文件名字 * ...

  7. 在vue中把数据导出Excel文件

    在vue中把数据导出Excel文件 第一次尝试写文章 在vue中把数据导出成Excel格式的文件,话不多,上代码: 第一步我们要先安装几个集成的插件 npm install -S file-saver ...

  8. JSON数据导出Excel表

    JSON数据导出Excel表 应老板要求写了一个实用的小功能,需求是将api接口返回的JSON数据导出Excel表,过程中改了两稿,一是前端js直接导出一个表,另一是分门别类在java中同时导出多个表 ...

  9. java批量文件打包成压缩成zip下载和大量数据导出excel时的处理方法

    对于我们来说,java导出数据成excel或其他数据文件,或者下载资源是开发中的家常便饭, 但是在导出的时候,如果点击一个按钮导出几百万条数据,如果不作处理的话很可能会出现一系列的问题. 这里介绍打包 ...

最新文章

  1. zoj 3627(贪心)
  2. HttpPrinter与YunPrinter区别
  3. Linux命令之乐--nmap
  4. 双11稳定性负责人叔同讲述:九年双11的云化架构演进和升级
  5. python显示图像某列的颜色值_Python Pandas Matplotlib图由单列中定义的类型值着色
  6. 七天学会SALTSTACK自动化运维 (3)
  7. D3DLOCK写纹理遇到的问题
  8. createjs入门
  9. IDEA连接Git后类的颜色含义
  10. PHP 中获取文件名及路径
  11. Discuz2.5菜鸟解析-2
  12. Android Screen Orientation
  13. hdu 1104 Remainder
  14. C语言例题——简易秒表
  15. 如何模拟自动鼠标点击或记录鼠标和回放
  16. Linux安装iso镜像中的软件
  17. Android动画特效第二弹——QQ聊天彩蛋蹦蹦哒
  18. Linux系统概念复习
  19. 餐饮门店每日业务情况数据分析仪表盘(外卖平台)
  20. grpc协议_gRPC和协议缓冲区简介

热门文章

  1. java string转elements_Java异常堆栈信息转String
  2. JPBC库应用之身份基加密IBE
  3. 注塑机压铸机比例阀控制放大器
  4. 基于Nacos配置中心实现Spring Cloud Gateway的动态路由管理
  5. 什么是ACL和RBAC
  6. nand2tetris 布尔函数和和门逻辑路线图
  7. 为什么Word字间距异常的大?怎么修正排版?
  8. Codeforces 868C - Qualification Rounds 思维
  9. Mybatis学习小结
  10. ARM CHI总线笔记