电脑维修主板会导致数据丢失吗
电脑故障大全---主板主板是整个电脑的关键部件,在电脑起着至关重要的作用。如果主板产生故障将会影响到整个PC机系统的工作。下面,我们就一起来看看主板在使用过程中最常见的故障有哪些。 常见故障一:开机...
2024.11.18最近我做过一个MySQL百万级别数据的excel导出功能,已经正常上线使用了。
这个功能挺有意思的,里面需要注意的细节还真不少,现在拿出来跟大家分享一下,希望对你会有所帮助。
原始需求:用户在UI界面上点击全部导出按钮,就能导出所有商品数据。
咋一看,这个需求挺简单的。
但如果我告诉你,导出的记录条数,可能有一百多万,甚至两百万呢?
这时你可能会倒吸一口气。
因为你可能会面临如下问题:
如果同步导数据,接口很容易超时。如果把所有数据一次性装载到内存,很容易引起OOM。数据量太大sql语句必定很慢。相同商品编号的数据要放到一起。如果走异步,如何通知用户导出结果?如果excel文件太大,目标用户打不开怎么办?我们要如何才能解决这些问题,实现一个百万级别的excel数据快速导出功能呢?
1.异步处理做一个MySQL百万数据级别的excel导出功能,如果走接口同步导出,该接口肯定会非常容易超时。
因此,我们在做系统设计的时候,第一选择应该是接口走异步处理。
说起异步处理,其实有很多种,比如:使用开启一个线程,或者使用线程池,或者使用job,或者使用mq等。
为了防止服务重启时数据的丢失问题,我们大多数情况下,会使用job或者mq来实现异步功能。
1.1 使用job如果使用job的话,需要增加一张执行任务表,记录每次的导出任务。
用户点击全部导出按钮,会调用一个后端接口,该接口会向表中写入一条记录,该记录的状态为:待执行。
有个job,每隔一段时间(比如:5分钟),扫描一次执行任务表,查出所有状态是待执行的记录。
然后遍历这些记录,挨个执行。
需要注意的是:如果用job的话,要避免重复执行的情况。比如job每隔5分钟执行一次,但如果数据导出的功能所花费的时间超过了5分钟,在一个job周期内执行不完,就会被下一个job执行周期执行。
所以使用job时可能会出现重复执行的情况。
为了防止job重复执行的情况,该执行任务需要增加一个执行中的状态。
具体的状态变化如下:
执行任务被刚记录到执行任务表,是待执行状态。当job第一次执行该执行任务时,该记录再数据库中的状态改为:执行中。当job跑完了,该记录的状态变成:完成或失败。这样导出数据的功能,在第一个job周期内执行不完,在第二次job执行时,查询待处理状态,并不会查询出执行中状态的数据,也就是说不会重复执行。
此外,使用job还有一个硬伤即:它不是立马执行的,有一定的延迟。
如果对时间不太敏感的业务场景,可以考虑使用该方案。
1.2 使用mq用户点击全部导出按钮,会调用一个后端接口,该接口会向mq服务端,发送一条mq消息。
有个专门的mq消费者,消费该消息,然后就可以实现excel的数据导出了。
相较于job方案,使用mq方案的话,实时性更好一些。
对于mq消费者处理失败的情况,可以增加补偿机制,自动发起重试。
RocketMQ自带了失败重试功能,如果失败次数超过了一定的阀值,则会将该消息自动放入死信队列。
2.使用easyexcel我们知道在Java中解析和生成Excel,比较有名的框架有Apache POI和jxl。
但它们都存在一个严重的问题就是:非常耗内存,POI有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
百万级别的excel数据导出功能,如果使用传统的Apache POI框架去处理,可能会消耗很大的内存,容易引发OOM问题。
而easyexcel重写了POI对07版Excel的解析,之前一个3M的excel用POI sax解析,需要100M左右内存,如果改用easyexcel可以降低到几M,并且再大的Excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。
需要在maven的pom.xml文件中引入easyexcel的jar包:
com.alibabaeasyexcel3.0.2复制代码之后,使用起来非常方便。
读excel数据非常方便:
@Testpublic void simpleRead() {String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();}复制代码写excel数据也非常方便:
@Testpublic void simpleWrite() {String fileName = TestFileUtil.getPath() + "write" + System.currentTimeMillis() + ".xlsx";// 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭// 如果这里想使用03 则 传入excelType参数即可EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());}复制代码easyexcel能大大减少占用内存的主要原因是:在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
3.分页查询百万级别的数据,从数据库一次性查询出来,是一件非常耗时的工作。
即使我们可以从数据库中一次性查询出所有数据,没出现连接超时问题,这么多的数据全部加载到应用服务的内存中,也有可能会导致应用服务出现OOM问题。
因此,我们从数据库中查询数据时,有必要使用分页查询。比如:每页5000条记录,分为200页查询。
public Page searchUser(SearchModel searchModel) {List userList = userMapper.searchUser(searchModel);Page pageResponse = Page.create(userList, searchModel);pageResponse.setTotal(userMapper.searchUserCount(searchModel));return pageResponse;}复制代码每页大小pageSize和页码pageNo,是SearchModel类中的成员变量,在创建searchModel对象时,可以设置设置这两个参数。
然后在Mybatis的sql文件中,通过limit语句实现分页功能:
limit #{pageStart}, #{pageSize}复制代码其中的pagetStart参数,是通过pageNo和pageSize动态计算出来的,比如:
pageStart = (pageNo - 1) * pageSize;复制代码4.多个sheet我们知道,excel对一个sheet存放的最大数据量,是有做限制的,一个sheet最多可以保存1048576行数据。否则在保存数据时会直接报错:
invalid row number (1048576) outside allowable range (0..1048575)复制代码如果你想导出一百万以上的数据,excel的一个sheet肯定是存放不下的。
因此我们需要把数据保存到多个sheet中。
5.计算limit的起始位置我之前说过,我们一般是通过limit语句来实现分页查询功能的:
limit #{pageStart}, #{pageSize}复制代码其中的pagetStart参数,是通过pageNo和pageSize动态计算出来的,比如:
pageStart = (pageNo - 1) * pageSize;复制代码如果只有一个sheet可以这么玩,但如果有多个sheet就会有问题。因此,我们需要重新计算limit的起始位置。
例如:
ExcelWriter excelWriter = EasyExcelFactory.write(out).build();int totalPage = searchUserTotalPage(searchModel);if(totalPage > 0) {Page page = Page.create(searchModel);int sheet = (totalPage % maxSheetCount == 0) ? totalPage / maxSheetCount: (totalPage / maxSheetCount) + 1;for(int i=0;i=startPageNo && page.getPageNo()男,1->女),需要自定义转换器,下面为自定义的GenderConverter代码实现;/** * excel性别转换器 * Created by macro on 2021/12/29. */public class GenderConverter implements Converter {@Overridepublic Class supportJavaTypeKey() {//对象属性类型return Integer.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {//CellData属性类型return CellDataTypeEnum.STRING;}@Overridepublic Integer convertToJavaData(ReadConverterContext context) throws Exception {//CellData转对象属性String cellStr = context.getReadCellData().getStringValue();if (StrUtil.isEmpty(cellStr)) return null;if ("男".equals(cellStr)) {return 0;} else if ("女".equals(cellStr)) {return 1;} else {return null;}}@Overridepublic WriteCellData convertToExcelData(WriteConverterContext context) throws Exception {//对象属性转CellDataInteger cellValue = context.getValue();if (cellValue == null) {return new WriteCellData("");}if (cellValue == 0) {return new WriteCellData("男");} else if (cellValue == 1) {return new WriteCellData("女");} else {return new WriteCellData("");}}}复制代码接下来我们在Controller中添加一个接口,用于导出会员列表到Excel,还需给响应头设置下载excel的属性,具体代码如下;/** * EasyExcel导入导出测试Controller * Created by macro on 2021/10/12. */@Controller@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")@RequestMapping("/easyExcel")public class EasyExcelController {@SneakyThrows(IOException.class)@ApiOperation(value = "导出会员列表Excel")@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)public void exportMemberList(HttpServletResponse response) {setExcelRespProp(response, "会员列表");List memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);EasyExcel.write(response.getOutputStream()).head(Member.class).excelType(ExcelTypeEnum.XLSX).sheet("会员列表").doWrite(memberList);}/*** 设置excel下载响应头属性*/private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=utf-8‘‘" + fileName + ".xlsx");}}复制代码运行项目,通过Swagger测试接口,注意在Swagger中访问接口无法直接下载,需要点击返回结果中的下载按钮才行,访问地址:http://localhost:8088/swagger-ui/下载完成后,查看下文件,一个标准的Excel文件已经被导出了。简单导入接下来我们以会员信息的导入为例,来体验下EasyExcel的导入功能。
在Controller中添加会员信息导入的接口,这里需要注意的是使用@RequestPart注解修饰文件上传参数,否则在Swagger中就没法显示上传按钮了;/** * EasyExcel导入导出测试Controller * Created by macro on 2021/10/12. */@Controller@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")@RequestMapping("/easyExcel")public class EasyExcelController {@SneakyThrows@ApiOperation("从Excel导入会员列表")@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)@ResponseBodypublic CommonResult importMemberList(@RequestPart("file") MultipartFile file) {List memberList = EasyExcel.read(file.getInputStream()).head(Member.class).sheet().doReadSync();return CommonResult.success(memberList);}}复制代码然后在Swagger中测试接口,选择之前导出的Excel文件即可,导入成功后会返回解析到的数据。复杂导出当然EasyExcel也可以实现更加复杂的导出,比如导出一个嵌套了商品信息的订单列表,下面我们来实现下!
使用EasyPoi实现之前我们使用过EasyPoi实现该功能,由于EasyPoi本来就支持嵌套对象的导出,直接使用内置的@ExcelCollection注解即可实现,非常方便也符合面向对象的思想。
寻找方案由于EasyExcel本身并不支持这种一对多的信息导出,所以我们得自行实现下,这里分享一个我平时常用的快速查找解决方案的办法。
我们可以直接从开源项目的issues里面去搜索,比如搜索下一对多,会直接找到有无一对多导出比较优雅的方案这个issue。
从此issue的回复我们可以发现,项目维护者建议创建自定义合并策略来实现,有位回复的老哥已经给出了实现代码,接下来我们就用这个方案来实现下。
解决思路为什么自定义单元格合并策略能实现一对多的列表信息的导出呢?首先我们来看下将嵌套数据平铺,不进行合并导出的Excel。
看完之后我们很容易理解解决思路,只要把订单ID相同的列中需要合并的列给合并了,就可以实现这种一对多嵌套信息的导出了。
实现过程首先我们得把原来嵌套的订单商品信息给平铺了,创建一个专门的导出对象OrderData,包含订单和商品信息,二级表头可以通过设置@ExcelProperty的value为数组来实现;/** * 订单导出 * Created by macro on 2021/12/30. */@Data@EqualsAndHashCode(callSuper = false)public class OrderData {@ExcelProperty(value = "订单ID")@ColumnWidth(10)@CustomMerge(needMerge = true, isPk = true)private String id;@ExcelProperty(value = "订单编码")@ColumnWidth(20)@CustomMerge(needMerge = true)private String orderSn;@ExcelProperty(value = "创建时间")@ColumnWidth(20)@DateTimeFormat("yyyy-MM-dd")@CustomMerge(needMerge = true)private Date createTime;@ExcelProperty(value = "收货地址")@CustomMerge(needMerge = true)@ColumnWidth(20)private String receiverAddress;@ExcelProperty(value = {"商品信息", "商品编码"})@ColumnWidth(20)private String productSn;@ExcelProperty(value = {"商品信息", "商品名称"})@ColumnWidth(20)private String name;@ExcelProperty(value = {"商品信息", "商品标题"})@ColumnWidth(30)private String subTitle;@ExcelProperty(value = {"商品信息", "品牌名称"})@ColumnWidth(20)private String brandName;@ExcelProperty(value = {"商品信息", "商品价格"})@ColumnWidth(20)private BigDecimal price;@ExcelProperty(value = {"商品信息", "商品数量"})@ColumnWidth(20)private Integer count;}复制代码然后将原来嵌套的Order对象列表转换为OrderData对象列表;/** * EasyExcel导入导出测试Controller * Created by macro on 2021/10/12. */@Controller@Api(tags = "EasyExcelController", description = "EasyExcel导入导出测试")@RequestMapping("/easyExcel")public class EasyExcelController {private List convert(List orderList) {List result = new ArrayList();for (Order order : orderList) {List productList = order.getProductList();for (Product product : productList) {OrderData orderData = new OrderData();BeanUtil.copyProperties(product,orderData);BeanUtil.copyProperties(order,orderData);result.add(orderData);}}return result;}}复制代码再创建一个自定义注解CustomMerge,用于标记哪些属性需要合并,哪个是主键;/** * 自定义注解,用于判断是否需要合并以及合并的主键 */@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface CustomMerge {/** * 是否需要合并单元格 */boolean needMerge() default false;/** * 是否是主键,即该字段相同的行合并 */boolean isPk() default false;}复制代码再创建自定义单元格合并策略类CustomMergeStrategy,当Excel中两列主键相同时,合并被标记需要合并的列;/** * 自定义单元格合并策略 */public class CustomMergeStrategy implements RowWriteHandler {/** * 主键下标 */private Integer pkIndex;/** * 需要合并的列的下标集合 */private List needMergeColumnIndex = new ArrayList();/** * DTO数据类型 */private Class elementType;public CustomMergeStrategy(Class elementType) {this.elementType = elementType;}@Overridepublic void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {// 如果是标题,则直接返回if (isHead) {return;}// 获取当前sheetSheet sheet = writeSheetHolder.getSheet();// 获取标题行Row titleRow = sheet.getRow(0);if (null == pkIndex) {this.lazyInit(writeSheetHolder);}// 判断是否需要和上一行进行合并// 不能和标题合并,只能数据行之间合并if (row.getRowNum()电脑故障大全---主板主板是整个电脑的关键部件,在电脑起着至关重要的作用。如果主板产生故障将会影响到整个PC机系统的工作。下面,我们就一起来看看主板在使用过程中最常见的故障有哪些。 常见故障一:开机...
2024.11.18现代的UEFI BIOS除了传统BIOS的一些配置信息还在CMOS中,绝大部分需要存储的内容都被保存在闪存芯片中,在那里,还居住着BIOS的执行代码、ME的代码和存储部分,以及一些其他固件们(GBe,...
2024.11.23电脑重置是什么意思重置此电脑就是恢复出厂设置的意思,可以初始化当前的系统,重新安装Windows;在重新安装系统的历程中可以挑选是否保留现有的软件,应用和资料。电脑重置系统和重装系统有区别吗答案是有的...
2024.11.22数据备份真的是很重要很重要的电脑使用习惯!第一站WIN系统设置还原点打开设置——系统——系统信息,选择红框中的【系统保护】。将系统盘C盘(如果有多个盘的话,建议全都开)的系统保护【启用】;启用系统保护...
2024.11.21来豹小程序内所有收集的内容,都支持导出。导出时可直接发送给微信好友,也可以下载到本地。操作教程只有页面发布人和管理员可查看导出数据,方法:点击数据统计-导出数据,选择您需要的导出方式,生成下载链接。生...
2024.11.23