万级数据优化EasyExcel+mybatis流式查询导出封装

文章目录

    • 前言.万级数据优化
    • 一. 直接上流式查询封装工具代码
    • 二. 传统分页导出查询
    • 三. 流式查询概念
    • 游标查询


前言.万级数据优化

我们不妨先给大家讲一个概念,利用此概念我们正好给大家介绍一个数据库优化的小技巧: 需求如下:将一个地市表的数据导出70万条。

在这里插入图片描述

如果你不假思索,直接一条sql语句搞上去,直接就会内存溢出,因为mysql会将结果记录统一查询出来然后返还给内存:那内存可能直接OOM!

@Test
public void testQuery1()  {// 1、定义资源Connection connection = null;ResultSet resultSet = null;PreparedStatement statement = null;String sql = "select * from user";try {// 获取连接connection = DBUtil.getConnection();// 获取使用预编译的statementstatement = connection.prepareStatement(sql);long start = System.currentTimeMillis();resultSet = statement.executeQuery();while (resultSet.next()){System.out.println("name---->" + resultSet.getString("nick_name") );}long end = System.currentTimeMillis();System.out.println(end -start);} catch (SQLException e){e.printStackTrace();} finally {// 关闭资源DBUtil.closeAll(connection,statement,resultSet);}
}

所以我们通常有如下几种解决方案:

一. 直接上流式查询封装工具代码

使用2核4G云服务器 下载速度在40-50s之间波动
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

表格美化 CustomCellWeightStrategy.class
/*** @author YuanJie* @projectName vector-server* @package com.vector.common.utils.easyexcel* @className com.vector.common.utils.easyexcel.CustomCellWeightStrategy* @copyright Copyright 2020 vector, Inc All rights reserved.* @date 2023/8/28 17:58*/
public class CustomCellWeightStrategy extends AbstractColumnWidthStyleStrategy {private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();@Overrideprotected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);if (needSetWidth) {Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>());int columnWidth = this.dataLength(cellDataList, cell, isHead)+8;if (columnWidth >= 0) {if (columnWidth > 254) {columnWidth = 254;}Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());if (maxColumnWidth == null || columnWidth > maxColumnWidth) {maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);Sheet sheet = writeSheetHolder.getSheet();sheet.setColumnWidth(cell.getColumnIndex(), columnWidth * 200);}//设置单元格类型cell.setCellType(CellType.STRING);// 数据总长度int length = cell.getStringCellValue().length();// 换行数int rows = cell.getStringCellValue().split("\n").length;// 默认一行高为20cell.getRow().setHeightInPoints(rows * 20);}}}/*** 计算长度** @param cellDataList* @param cell* @param isHead* @return*/private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {if (isHead) {return cell.getStringCellValue().getBytes().length;} else {CellData<?> cellData = cellDataList.get(0);CellDataTypeEnum type = cellData.getType();if (type == null) {return -1;} else {switch (type) {case STRING:// 换行符(数据需要提前解析好)int index = cellData.getStringValue().indexOf("\n");return index != -1 ?cellData.getStringValue().substring(0, index).getBytes().length + 1 : cellData.getStringValue().getBytes().length + 1;case BOOLEAN:return cellData.getBooleanValue().toString().getBytes().length;case NUMBER:return cellData.getNumberValue().toString().getBytes().length;default:return -1;}}}}
}
封装工具类EasyExcelUtil.class
/*** @author YuanJie* @projectName vector-server* @package com.vector.common.utils* @className com.vector.common.utils.easyexcel.EasyExcelUtil* @copyright Copyright 2020 vector, Inc All rights reserved.* @date 2023/8/26 1:17*/
@Slf4j
public class EasyExcelUtil {private static final String DATE_FORMAT = "yyyy-MM-dd";/*** 设置批量存储最大值,也影响sheet页数*/private static final Integer MAX_SHEET_DATA = 50000;/*** 设置内存最大值*/private static final Integer MAX_MEMORY_DATA = 1000;/*** 使用EasyExcel生成Excel  xls** @param response      响应对象* @param fileNameParam 文件名* @param sheetName     表格名* @param clazz         导出实体类* @param t             查库入参* @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);*/public static <T> void writeExcelXls(HttpServletResponse response, String fileNameParam,String sheetName, Class<?> clazz, T t,Function<T, Cursor<?>> func) throws Exception {streamExportExcel(response, fileNameParam, sheetName, clazz, ExcelTypeEnum.XLS.getValue(), t, func);}/*** 使用EasyExcel生成Excel  xlsx** @param response      响应对象* @param fileNameParam 文件名* @param sheetName     表格名* @param clazz         导出实体类* @param t             查库入参* @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);*/public static <T> void writeExcelXlsx(HttpServletResponse response, String fileNameParam,String sheetName, Class<?> clazz, T t,Function<T, Cursor<?>> func) throws Exception {streamExportExcel(response, fileNameParam, sheetName, clazz, ExcelTypeEnum.XLSX.getValue(), t, func);}/*** 流式导出 Excel** @param response      响应对象* @param fileNameParam 文件名* @param sheetName     表格名* @param clazz         导出实体类* @param excelType     导出类型* @param t             查库入参* @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);* @throws Exception 异常*/private static <T> void streamExportExcel(HttpServletResponse response, String fileNameParam,String sheetName, Class<?> clazz, String excelType,T t, Function<T, Cursor<?>> func) throws Exception {DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT);String fileName = fileNameParam + dateTimeFormatter.format(LocalDateTime.now()) + excelType;ExcelWriter excelWriter = EasyExcel.write(getOutputStream(fileName, response, excelType), clazz).registerWriteHandler(new CustomCellWeightStrategy()).build();// 内容样式WriteCellStyle contentWriteCellStyle = new WriteCellStyle();// 水平居中contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);// 垂直居中contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);// 设置自动换行,前提内容中需要加「\n」才有效contentWriteCellStyle.setWrapped(true);// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现HorizontalCellStyleStrategy horizontalCellStyleStrategy =new HorizontalCellStyleStrategy(null, contentWriteCellStyle);Cursor<?> cursor;List<Object> list = new ArrayList<>();int page = 0;WriteSheet writeSheet = EasyExcel.writerSheet(++page, sheetName + page).registerWriteHandler(horizontalCellStyleStrategy).build();// 流式数据库查询cursor = func.apply(t);int count = 0;try {for (Object o : cursor) {list.add(o);if(list.size() == MAX_MEMORY_DATA){count += list.size();excelWriter.write(list, writeSheet);list.clear();// 每个sheet页最大存储MAX_SHEET_DATA条数据if (count >= MAX_SHEET_DATA) {writeSheet = EasyExcel.writerSheet(++page, sheetName + page).registerWriteHandler(horizontalCellStyleStrategy).build();}}}// 处理最后不足MAX_SHEET_DATA的数据if (list.size() > 0) {writeSheet = EasyExcel.writerSheet(++page, sheetName + page).registerWriteHandler(horizontalCellStyleStrategy).build();excelWriter.write(list, writeSheet);list.clear();}} catch (Exception e) {// 重置responseresponse.reset();response.setContentType("application/json");response.setCharacterEncoding("utf-8");String json = JacksonInstance.toJson(R.errorResult(EnumHttpCode.SYSTEM_ERROR, "下载文件失败" + e.getMessage()));response.getWriter().println(json);} finally {if (cursor != null) {cursor.close();}if (excelWriter != null) {excelWriter.finish();}}}/*** 导出文件时为Writer生成OutputStream** @param finalName 文件名* @param response 响应对象* @param excelType 导出文件类型* @return OutputStream*/private static OutputStream getOutputStream(String finalName, HttpServletResponse response, String excelType) throws Exception {response.reset();finalName = URLEncoder.encode(finalName, StandardCharsets.UTF_8);if (ExcelTypeEnum.XLS.getValue().equals(excelType)) {response.setContentType("application/vnd.ms-excel");} else if (ExcelTypeEnum.XLSX.getValue().equals(excelType)) {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");}response.setCharacterEncoding("utf8");response.setHeader("Content-Disposition", "attachment; filename=" + finalName);response.setHeader("Pragma", "public");response.setHeader("Cache-Control", "no-store");response.addHeader("Cache-Control", "max-age=0");return response.getOutputStream();}
}
导出实体 Dto.class
@Data
public class Dto {@NumberFormat("#")@ExcelProperty(value = "地市编码", index = 0)Long code;@ExcelProperty(value = "地市名称", index = 1)String name;@NumberFormat("#")@ExcelProperty(value = "地市级别", index = 2)Integer level;@NumberFormat("#")@ExcelProperty(value = "地市父编码", index = 3)Long pcode;@NumberFormat("#")@ExcelProperty(value = "地市父名称", index = 4)Integer category;
}
测试用例 MeTestController.class
    @Resourceprivate ExportMapper exportMapper;@Resourceprivate HttpServletRequest request;@Resourceprivate HttpServletResponse response;@GetMapping("/export")@Transactionalpublic void export() throws Exception {Long params = 110101001000L;EasyExcelUtil.writeExcelXlsx(response,"地市信息","地市区域",Dto.class,params,param -> exportMapper.export(null));}
测试Dao ExportMapper.class
public interface ExportMapper {@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)@ResultType(Dto.class)@Select("select * from area_code_2023")Cursor<Dto> export(@Param("params") Long params);
}

二. 传统分页导出查询

大表的深度分页性能很差,也受制于表设计的影响
@Test
public void testQuery2()  {// 1、定义资源Connection connection = null;ResultSet resultSet = null;PreparedStatement statement = null;String sql = "select * from user limit ?,?";try {// 获取连接connection = DBUtil.getConnection();// 获取使用预编译的statementstatement = connection.prepareStatement(sql);// 获取结果集long start = System.currentTimeMillis();long begin = 0L, offset = 10000L;while (true){statement.setLong(1,begin);statement.setLong(2,offset);begin += offset;resultSet = statement.executeQuery();boolean flag = resultSet.next();if(!flag) break;while (flag){System.out.println("name---->" + resultSet.getString("nick_name") );flag = resultSet.next();}}long end = System.currentTimeMillis();System.out.println(end -start);} catch (SQLException e){e.printStackTrace();} finally {// 关闭资源DBUtil.closeAll(connection,statement,resultSet);}
}

三. 流式查询概念

采用传统的Stream流式思想,将直接提供数据替换成提供获取数据的管道,客户端读取数据时直接从管道中遍历获取;整个读取的过程需要客户端保持和服务端的连接,也很好理解,它实际是一个管道,管道得通着才能取数据。

采用传统的Stream流式思想,将直接提供数据替换成提供获取数据的管道,客户端读取数据时直接从管道中遍历获取;整个读取的过程需要客户端保持和服务端的连接,也很好理解,它实际是一个管道,管道得通着才能取数据。

流式查询有两种使用方式,一种是用Cursor作为返回值,对数据进行遍历操作;一种是不设置返回值,在入参中传入一个ResultHandler作为回调处理数据。本文将基于Mybatis具体介绍使用方法。 这两种返回值的使用方式是相似的,唯一区别就是返回值不同。Mybatis查询有两种方式,一种是基于注解加在Mapper接口上方,一种是写在xml文件中,主要需要设置以下几个属性:
ResultSetType结果集读取方式
FetchSizeMySQL服务端单次发送至客户端的数据条数
ResultType这个眼熟吧,设置返回实体类映射

ResultSetType有4种可选项

DEFAULT(-1),
FORWARD_ONLY(1003),
SCROLL_INSENSITIVE(1004),
SCROLL_SENSITIVE(1005);

FORWARD_ONLY顾名思义只能向前,即数据只能向前读取,是不是就类似一个流水的管道,读一条就相当于水流过去一些。也是我们需要选用的。
SCROLL_INSENSITIVE不敏感滚动,和下面那个差不多,都是可以向后读或向前读;这意味着已读取过的数据不能丢掉,要继续保存在内存中,因为有可能会回去再次读取他们。
SCROLL_SENSITIVE敏感滚动,和上面那个差不多。
这么一比较就看得出来,当选的一定是FORWARD_ONLY,我们亟需解决的就是大数据量对内存的影响,再用后面两个还是会放在内存中。

FetchSize这个概念在许多服务中都有提及,例如RabbitMQ中是消费者取过来预处理的消息数量,但在MySQL中完全不是一个概念。MySQL的数据传输是基于C/S的阻塞机制,即Client设置FetchSize = 1000,而Server查出来10000条数据,按照常理应该是Server智能地使用分页策略1000条1000条取;实际不是,Server查出来多少就是多少,他会放在自己特定的内存空间内,只是会根据FetchSize的大小一点一点传送给Client——利用C/S的通讯阻塞,发1000条、堵一下、发1000条、堵一下……。

JDBC官方给出的答案是设置为“Integer.MIN_VALUE”,具体原因不清楚,但我猜是为了和游标查询区分开,因为一会你会发现流式查询和游标查询唯一的区别就是FetchSize的大小。

注解式

@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(ResultVo.class)
@Select("SELECT *, 0 orderType FROM `table`\n" +"        WHERE username = #{userName}")
Cursor<ResultVo> listOrders(@Param("userName") String userName);@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(ResultVo.class)
@Select("SELECT *, 0 orderType FROM `table`\n" +"        WHERE username = #{userName}")
void listOrders2(@Param("userName") String userName, ResultHandler<ResultVo> handler);

使用Mybatis的注解,在 @Options 中指定查询配置参数,在@ResultType中指定返回值类型 ,在 @Select中指定查询语句。最后用Cursor接收返回值,Cursor是可遍历的,所以直接Foreach遍历即可;或者返回void 用ResultHandler处理数据回调,在调用方式时传入new ResultHandler并写明处理逻辑。

xml式

<select id="listOrders" resultType="com.vo.ResultVo" resultSetType="FORWARD_ONLY" fetchSize="Integer.MIN_VALUE">SELECT *, 1 stuffCount, 1 orderType FROM `table`WHERE username = #{userName}
</select>

需要注意的是,不可以注解 + xml混合使用,比如注解指定fetchSize,xml只写查询语句,这种只有xml语句会生效!!!要不全用注解,要不全用xml!!!

流式查询由于需要保持客户端与服务端的连接,而一般查询提交完连接就会关闭;因此我们需要保持事务开启,否则会报“A Cursor is already closed.”,即Cursor已经关闭,没法再读取了。最简单的方法就是在方法上加@Transactional,在查询完毕以前事务会一直持有这个数据库连接,但我们在使用完毕后也要自行关闭连接,显式调用Cursor.close(),或者用try with resource语句。

游标查询和流式查询的区分是fetchSize = Integer.MIN_VALUE

Cursor 还提供了三个方法:

  1. isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  2. isConsumed():用于判断查询结果是否全部取完;
  3. getCurrentIndex():返回已经获取了多少条数据。
try(Cursor cursor = OrderMapper.listOrders()) {cursor.forEach(rowObject -> {// ...});
}OrderMapper.listOrders2(queryWrapper,resultContext -> {ResultVo result = resultContext.getResultObject();//这边循环调用就可以实现业务了}

游标查询

和流式查询类似fetchSize不设置为MIN_VALUE即可
JDBC查询默认是不支持FetchSize属性的,需要在JDBC连接URL后面加上**“useCursorFetch=true”。**

useCursorFetch=true 是针对 MySQL 数据库的 JDBC 连接参数,用于启用服务器端游标获取数据。在 MyBatis 中,当使用流式查询(例如:分页查询、结果集处理和使用游标等)时,这个配置可以帮助逐行从服务器检索数据,而不是一次性将所有数据加载到内存中,从而降低内存占用。

当使用 MySQL 数据库时,在 JDBC 连接字符串中加入 useCursorFetch=true,并结合设置合适的 fetchSize,可以避免因一次性加载过多数据导致的内存溢出问题。注意,此配置仅对 MySQL 数据库有效。 如果不设置 useCursorFetch=true 这个配置,仅使用之前提到的那些配置(如设置 defaultFetchSize、分页查询、结果集处理和使用游标等),在大多数情况下,这些配置仍然可以有效地避免查询导致的内存溢出。

但需要注意的是,对于 MySQL 数据库,如果不启用服务器端游标获取数据,这可能会影响到流式查询的效果。因为在默认情况下,MySQL JDBC 驱动会一次性将所有数据加载到内存中。此时,即使使用了其他配置,也可能无法达到预期的内存优化效果。

总的来说,在使用 MySQL 数据库时,推荐在 JDBC 连接字符串中加入 useCursorFetch=true 配置,以更好地支持流式查询和降低内存占用。在其他数据库中,可以根据实际需求和场景选择合适的配置和策略来避免查询导致的内存溢出。

还要知道如何判断自己是否使用了流式查询或游标查询,下面是几个数据集的对应关系

普通分页ResultsetRowsStaticRowDataStatic
查询方式结果集类型行数据类型
流式查询ResultsetRowsStreamingRowDataDynamic
游标查询ResultsetRowsCursorRowDataCursor

这3种查询方式,常规非大数据模式下普通查询最快,其次是流式查询,最次是游标查询.

主要是由于游标查询需要和数据库进行多次网络交互,Client处理完这部分后再拉取下一部分数据,因此会比较慢。但是流式查询又会长时间占用同一个数据库连接,因此要取舍一下是能接受连接一直持有但是可能会堵住导致响应慢,还是可能占用较多连接数但单次响应快。当通过流式查询获取一个ResultSet后,在你通过next迭代出所有元素之前或者调用close关闭它之前,你不能使用同一个数据库连接去发起另外一个查询,否者抛出异常(第一次调用的正常,第二次的抛出异常)。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

创建型模式-建造者模式

使用多个简单的对象一步一步构建成一个复杂的对象 主要解决&#xff1a;主要解决在软件系统中&#xff0c;有时候面临着"一个复杂对象"的创建工作&#xff0c;其通常由各个部分的子对象用一定的算法构成&#xff1b;由于需求的变化&#xff0c;这个复杂对象的各个部…

Linux内核源码分析 (5)多处理器调度

Linux内核源码分析 (5)多处理器调度 文章目录 Linux内核源码分析 (5)多处理器调度注&#xff1a;本章节使用的内核版本为Linux 5.6.18一、 SMT和NUMA1、SMP (对称多处理器结构)2、NUMA &#xff08;非一致内存访问结构&#xff09; 二、多核调度三、调度域和调度组四、SMP调度详…

为了更好和大家交流,欢迎大家加我的微信账户

因为一些懂的都懂的原因&#xff0c;如果我的账户显示为 此时我无法通过站内信、留言或者任何方式和大家联系。 如果看到这样的内容&#xff0c;可以在此评论区留下你的微信账户&#xff0c;我看到后会添加你。为防止其他人冒充我&#xff0c;我的微信号以2206结尾。

Docker harbor 私有仓库的部署和管理

目录 一、什么是Harbor 二、Harbor的特性 三、Harbor的构成 四、部署配置Docker Harbor 1. 首先需要安装 Docker-Compose 服务 2.部署 Harbor 服务 3.使用harbor仓库 &#xff08;1&#xff09;项目管理 &#xff08;2&#xff09;用户管理 一、什么是Harbor Harbor …

【IOTE】物联网射频模组和芯片级方案提供商——深圳信驰达科技将精彩亮相IOTE物联网展

►►►强势来袭 Strong Attack 主物联场&#xff0c;相约深圳&#xff1b;2023&#xff0c;共论商机&#xff01;IOTE2023第二十届国际物联网展深圳站将于2023年9月20-22日在深圳国际会展中心(宝安新馆)开展&#xff01;汇聚全球超800家参展企业&#xff0c;呈现更多数字化纷呈…

从零开发JavaWeb入门项目--十天掌握

原文网址&#xff1a;从零开发JavaWeb入门项目--十天掌握_IT利刃出鞘的博客-CSDN博客 简介 这是一个靠谱的JavaWeb入门项目实战&#xff0c;名字叫蚂蚁爱购。从零开发项目&#xff0c;视频加文档&#xff0c;十天就能学会开发JavaWeb项目&#xff0c;教程路线是&#xff1a;搭…

uni-app中使用iconfont彩色图标

uni-app中使用iconfont彩色图标 大家好&#xff0c;今天我们来学习一下uni-app中使用iconfont彩色图标&#xff0c;好好看&#xff0c;好好学&#xff0c;超详细的 第一步 首先&#xff0c;从iconfont官网&#xff08;iconfont-阿里巴巴矢量图标库&#xff09;选择自己需要的图…

Leetcode110. 平衡二叉树

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 给定一个二叉树&#xff0c;判断它是否是高度平衡的二叉树。 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a; 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。 题解&#xff…

Linux测开常用命令总结

文章目录 Linux系统中文件目录树 基本指令的使用&#xff1a; Linux命令的帮助信息查看 --help command --help 说明&#xff1a; 显示command 命令的帮助信息通过man命令查看帮助信息 man command( 命令的名称) man 命令查看的帮助信息更加详细ls&#xff0c;pwd&#xff0c…

原生js实现轮播图及无缝滚动

我这里主要说轮播图和无缝滚动的实现思路&#xff0c;就采用最简单的轮播图了&#xff0c;当然实现的思路有很多种&#xff0c;我这也只是其中一种。 简单轮播图的大概结构是这样的&#xff0c;中间是图片&#xff0c;二边是箭头可以用来切换图片&#xff0c;下面的小圆点也可以…

代码随想录笔记--栈与队列篇

目录 1--用栈实现队列 2--用队列实现栈 3--有效的括号 4--删除字符串中的所有相邻重复项 5--逆波兰表达式求值 6--滑动窗口的最大值 7--前k个高频元素 1--用栈实现队列 利用两个栈&#xff0c;一个是输入栈&#xff0c;另一个是输出栈&#xff1b; #include <iostrea…

如何把本地项目上传github

一、在gitHub上创建新项目 【1】点击添加&#xff08;&#xff09;-->New repository 【2】填写新项目的配置项 Repository name&#xff1a;项目名称 Description &#xff1a;项目的描述 Choose a license&#xff1a;license 【3】点击确定&#xff0c;项目已在githu…