【文件增量备份系统】使用Mysql的流式查询优化数据清理性能(针对百万量级数据)

本文主要是介绍【文件增量备份系统】使用Mysql的流式查询优化数据清理性能(针对百万量级数据),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 功能介绍
  • 原始方案
    • 测试
  • 流式处理
    • 测试
  • 功能可用性测试

功能介绍

清理功能的作用是:扫描数据库中已经备份过的文件,查看数据源中是否还有相应的文件,如果没有,说明该文件被删除了,那相应的,也需要将备份目标目录的文件以及相关的备份记录都一并删除掉

原始方案

使用分批处理,避免单次加载表中的所有数据,导致发现内存溢出,每次从备份文件表中查询出2000条备份文件记录,然后对查出来的数据进行检验、清理

/*** 检查数据,删除 无效备份信息 和 已备份文件* 什么叫无效?简单来说就是,已备份文件和原文件对应不上,或者说原文件被删除了** @param sourceId*/
@Override
public void clearBySourceIdv1(Long sourceId) {long current = 1;ClearTask clearTask = new ClearTask();clearTask.setId(snowFlakeUtil.nextId());// 填充数据源相关信息BackupSource source = backupSourceService.getById(sourceId);if (source == null) {throw new ClientException("所需要清理的数据源不存在");}clearTask.setClearSourceRoot(source.getRootPath());// 存储要删除的文件List<Long> removeBackupFileIdList = new ArrayList<>();List<String> removeBackupTargetFilePathList = new ArrayList<>();BackupFileRequest backupFileRequest = new BackupFileRequest();backupFileRequest.setBackupSourceId(sourceId);backupFileRequest.setSize(2000L);long totalFileNum = -1;long finishFileNum = 0;ClearStatistic clearStatistic = new ClearStatistic(0);while (true) { 查询数据,监测看哪些文件需要被删除// 分页查询出数据,即分批检查,避免数据量太大,占用太多内存backupFileRequest.setCurrent(current);PageResponse<BackupFile> backupFilePageResponse = backupFileService.pageBackupFile(backupFileRequest);if (totalFileNum == -1 && backupFilePageResponse.getTotal() != null) {totalFileNum = backupFilePageResponse.getTotal();Map<String, Object> dataMap = new HashMap<>();dataMap.put("code", WebsocketNoticeEnum.CLEAR_START.getCode());dataMap.put("message", WebsocketNoticeEnum.CLEAR_START.getDetail());clearTask.setTotalFileNum(totalFileNum);clearTask.setFinishFileNum(0L);clearTask.setClearStatus(0);clearTask.setClearNumProgress("0.0");clearTask.setStartTime(new DateTime());clearTask.setClearTime(0L);dataMap.put("clearTask", clearTask);webSocketServer.sendMessage(JSON.toJSONString(dataMap), WebSocketServer.usernameAndSessionMap.get("Admin"));}if (backupFilePageResponse.getRecords().size() > 0) {for (BackupFile backupFile : backupFilePageResponse.getRecords()) {// 获取备份文件的路径// todo 待优化为存储的时候,不存储整一个路径,节省数据库空间,只存储从根目录开始后面的路径,后面获取整个路径再进行拼接String sourceFilePath = backupFile.getSourceFilePath();File sourceFile = new File(sourceFilePath);if (!sourceFile.exists()) {// --if-- 如果原目录该文件已经被删除,则删除removeBackupFileIdList.add(backupFile.getId());removeBackupTargetFilePathList.add(backupFile.getTargetFilePath());}}// 换一页来检查current += 1;} else {// 查不出数据了,说明检查完了break;} 执行删除if (removeBackupFileIdList.size() > 0) {// 批量删除无效备份文件backupFileService.removeByIds(removeBackupFileIdList);// 删除无效的已备份文件for (String backupTargetFilePath : removeBackupTargetFilePathList) {File removeFile = new File(backupTargetFilePath);if (removeFile.exists()) {boolean delete = FileUtils.recursionDeleteFiles(removeFile, clearStatistic);if (!delete) {throw new ServiceException("文件无法删除");}}}// 批量删除无效备份文件对应的备份记录backupFileHistoryService.removeByFileIds(removeBackupFileIdList);removeBackupFileIdList.clear();removeBackupTargetFilePathList.clear();}// 告诉前端,更新清理状态finishFileNum += backupFilePageResponse.getRecords().size();Map<String, Object> dataMap = new HashMap<>();dataMap.put("code", WebsocketNoticeEnum.CLEAR_PROCESS.getCode());dataMap.put("message", WebsocketNoticeEnum.CLEAR_PROCESS.getDetail());clearTask.setFinishFileNum(finishFileNum);clearTask.setClearStatus(1);clearTask.setFinishDeleteFileNum(clearStatistic.finishDeleteFileNum);setClearProgress(clearTask, dataMap);}// 清理成功Map<String, Object> dataMap = new HashMap<>();dataMap.put("code", WebsocketNoticeEnum.CLEAR_SUCCESS.getCode());dataMap.put("message", WebsocketNoticeEnum.CLEAR_SUCCESS.getDetail());clearTask.setFinishFileNum(finishFileNum);clearTask.setClearStatus(2);clearTask.setFinishDeleteFileNum(clearStatistic.finishDeleteFileNum);setClearProgress(clearTask, dataMap);dataMap.put("clearTask", clearTask);
}

测试

经过测试,发现该方案非常慢,清理进度10%竟要花费3分钟

在这里插入图片描述

通过观察,发现备份文件数量一共有接近三百多万条,如此大的数据量,使用分页查询的性能会非常差。这是因为每次分页查询,都需要从头开始扫描,若分页的页码越大, 分页查询的速度也会越慢

在这里插入图片描述

在这里插入图片描述

流式处理

流式处理方式即使用数据库的流式查询功能,查询成功之后不是返回一个数据集合,而是返回一个迭代器,通过这个迭代器可以进行循环,每次查询出一条数据来进行处理。使用该方式可以有效降低内存占用,且因为不需要像分页一样每次重头扫描表,每查询一条数据都是在上次查询的基础上面查询,即知道上条数据的位置,因此查询效率较高

/*** 流式处理* 检查数据,删除 无效备份信息 和 已备份文件* 什么叫无效?简单来说就是,已备份文件和原文件对应不上,或者说原文件被删除了** @param sourceId*/
@SneakyThrows
public void clearBySourceIdV2(Long sourceId) {// 获取 dataSource Bean 的连接@Cleanup Connection conn = dataSource.getConnection();@Cleanup Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);stmt.setFetchSize(Integer.MIN_VALUE);long start = System.currentTimeMillis();// 查询sql,只查询关键的字段String sql = "SELECT id,source_file_path,target_file_path FROM backup_file where backup_source_id = " + sourceId;@Cleanup ResultSet rs = stmt.executeQuery(sql);loopResultSetProcessClear(rs, sourceId);log.info("流式清理花费时间:{} s ", (System.currentTimeMillis() - start) / 1000);
}/*** 循环读取,每次读取一行数据进行处理** @param rs* @param sourceId* @return*/
@SneakyThrows
private Long loopResultSetProcessClear(ResultSet rs, Long sourceId) {// 填充数据源相关信息BackupSource source = backupSourceService.getById(sourceId);if (source == null) {throw new ClientException("所需要清理的数据源不存在");}// 中途用来存储需要删除的文件信息List<Long> removeBackupFileIdList = new ArrayList<>();List<String> removeBackupTargetFilePathList = new ArrayList<>();// 查询文件总数long totalFileNum = backupFileService.count(Wrappers.query(new BackupFile()).eq("backup_source_id", sourceId));// 已经扫描的文件数量long finishFileNum = 0;ClearStatistic clearStatistic = new ClearStatistic(0);long second = System.currentTimeMillis() / 1000;long curSecond;// 发送消息通知前端 清理正式开始ClearTask clearTask = ClearTask.builder().id(snowFlakeUtil.nextId()).clearSourceRoot(source.getRootPath()).totalFileNum(totalFileNum).finishFileNum(0L).clearStatus(0).clearNumProgress("0.0").startTime(new DateTime()).clearTime(0L).build();Map<String, Object> dataMap = new HashMap<>();dataMap.put("clearTask", clearTask);notify(WebsocketNoticeEnum.CLEAR_START, dataMap);// 每次获取一行数据进行处理,rs.next()如果有数据返回true,否则返回falsewhile (rs.next()) {// 获取数据中的属性long fileId = rs.getLong("id");String sourceFilePath = rs.getString("source_file_path");String targetFilePath = rs.getString("target_file_path");// 所扫描的文件数量+1finishFileNum++;// 获取备份文件的路径File sourceFile = new File(sourceFilePath);if (!sourceFile.exists()) {// --if-- 如果原目录该文件已经被删除,则删除removeBackupFileIdList.add(fileId);removeBackupTargetFilePathList.add(targetFilePath);}if (removeBackupFileIdList.size() >= 2000) {clear(removeBackupFileIdList, removeBackupTargetFilePathList, clearStatistic);}curSecond = System.currentTimeMillis() / 1000;if (curSecond > second) {second = curSecond;// 告诉前端,更新清理状态clearTask.setFinishFileNum(finishFileNum);clearTask.setClearStatus(1);clearTask.setFinishDeleteFileNum(clearStatistic.finishDeleteFileNum);setClearProgress(clearTask, dataMap);notify(WebsocketNoticeEnum.CLEAR_PROCESS, dataMap);}}// 循环结束之后,再清理一次,避免文件数没有到达清理批量导致清理失败clear(removeBackupFileIdList, removeBackupTargetFilePathList, clearStatistic);// 告诉前端,清理成功clearTask.setFinishFileNum(finishFileNum);clearTask.setClearStatus(2);clearTask.setFinishDeleteFileNum(clearStatistic.finishDeleteFileNum);setClearProgress(clearTask, dataMap);notify(WebsocketNoticeEnum.CLEAR_SUCCESS, dataMap);return 0L;
}/*** 执行清理* @param removeBackupFileIdList* @param removeBackupTargetFilePathList* @param clearStatistic*/
private void clear(List<Long> removeBackupFileIdList, List<String> removeBackupTargetFilePathList, ClearStatistic clearStatistic) {// 批量删除无效备份文件backupFileService.removeByIds(removeBackupFileIdList);// 删除无效的已备份文件for (String backupTargetFilePath : removeBackupTargetFilePathList) {File removeFile = new File(backupTargetFilePath);if (removeFile.exists()) {boolean delete = FileUtils.recursionDeleteFiles(removeFile, clearStatistic);if (!delete) {throw new ServiceException("文件无法删除");}}}// 批量删除无效备份文件对应的备份记录backupFileHistoryService.removeByFileIds(removeBackupFileIdList);removeBackupFileIdList.clear();removeBackupTargetFilePathList.clear();
}/*** 发送通知给前端** @param noticeEnum* @param dataMap*/
private void notify(WebsocketNoticeEnum noticeEnum, Map<String, Object> dataMap) {dataMap.put("code", noticeEnum.getCode());dataMap.put("message", noticeEnum.getDetail());webSocketServer.sendMessage(JSON.toJSONString(dataMap), WebSocketServer.usernameAndSessionMap.get("Admin"));
}

测试

经过测试,发现改进后的程序只需要70秒就可以完成清理,速度是原始方案的25倍左右

在这里插入图片描述

功能可用性测试

初始状态,固态硬盘中文件目录结构如下图所示:

在这里插入图片描述

在数据源目录中添加如下文件夹和文件

在这里插入图片描述

备份结束后,数据源中新创建的数据被同步到固态硬盘中

在这里插入图片描述

在这里插入图片描述

在数据源中删除测试文件

在这里插入图片描述

成功清理了两个文件

在这里插入图片描述

固态硬盘中的数据成功被清理

在这里插入图片描述

这篇关于【文件增量备份系统】使用Mysql的流式查询优化数据清理性能(针对百万量级数据)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/786145

相关文章

mysqld_multi在Linux服务器上运行多个MySQL实例

《mysqld_multi在Linux服务器上运行多个MySQL实例》在Linux系统上使用mysqld_multi来启动和管理多个MySQL实例是一种常见的做法,这种方式允许你在同一台机器上运行多个... 目录1. 安装mysql2. 配置文件示例配置文件3. 创建数据目录4. 启动和管理实例启动所有实例

Java function函数式接口的使用方法与实例

《Javafunction函数式接口的使用方法与实例》:本文主要介绍Javafunction函数式接口的使用方法与实例,函数式接口如一支未完成的诗篇,用Lambda表达式作韵脚,将代码的机械美感... 目录引言-当代码遇见诗性一、函数式接口的生物学解构1.1 函数式接口的基因密码1.2 六大核心接口的形态学

使用DeepSeek API 结合VSCode提升开发效率

《使用DeepSeekAPI结合VSCode提升开发效率》:本文主要介绍DeepSeekAPI与VisualStudioCode(VSCode)结合使用,以提升软件开发效率,具有一定的参考价值... 目录引言准备工作安装必要的 VSCode 扩展配置 DeepSeek API1. 创建 API 请求文件2.

使用TomCat,service输出台出现乱码的解决

《使用TomCat,service输出台出现乱码的解决》本文介绍了解决Tomcat服务输出台中文乱码问题的两种方法,第一种方法是修改`logging.properties`文件中的`prefix`和`... 目录使用TomCat,service输出台出现乱码问题1解决方案问题2解决方案总结使用TomCat,

解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题

《解决IDEA使用springBoot创建项目,lombok标注实体类后编译无报错,但是运行时报错问题》文章详细描述了在使用lombok的@Data注解标注实体类时遇到编译无误但运行时报错的问题,分析... 目录问题分析问题解决方案步骤一步骤二步骤三总结问题使用lombok注解@Data标注实体类,编译时

Java中注解与元数据示例详解

《Java中注解与元数据示例详解》Java注解和元数据是编程中重要的概念,用于描述程序元素的属性和用途,:本文主要介绍Java中注解与元数据的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参... 目录一、引言二、元数据的概念2.1 定义2.2 作用三、Java 注解的基础3.1 注解的定义3.2 内

将sqlserver数据迁移到mysql的详细步骤记录

《将sqlserver数据迁移到mysql的详细步骤记录》:本文主要介绍将SQLServer数据迁移到MySQL的步骤,包括导出数据、转换数据格式和导入数据,通过示例和工具说明,帮助大家顺利完成... 目录前言一、导出SQL Server 数据二、转换数据格式为mysql兼容格式三、导入数据到MySQL数据

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

C++中使用vector存储并遍历数据的基本步骤

《C++中使用vector存储并遍历数据的基本步骤》C++标准模板库(STL)提供了多种容器类型,包括顺序容器、关联容器、无序关联容器和容器适配器,每种容器都有其特定的用途和特性,:本文主要介绍C... 目录(1)容器及简要描述‌php顺序容器‌‌关联容器‌‌无序关联容器‌(基于哈希表):‌容器适配器‌:(

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一