【文件增量备份系统】使用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倍左右

在这里插入图片描述

功能可用性测试

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在数据源中删除测试文件

在这里插入图片描述

成功清理了两个文件

在这里插入图片描述

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

在这里插入图片描述

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

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

相关文章

11 OpenCV 上采样与降采样,高斯不同(DOG)

文章目录 算子什么是高斯不同示例 算子 pyrUp(Mat src, Mat dst, Size(src.cols*2, src.rows*2)) 生成的图像是原图在宽与高各放大两倍 pyrDown(Mat src, Mat dst, Size(src.cols/2, src.rows/2)) 生成的图像是原图在宽与高各缩小1/2什么是高斯不同 定义&#xff1a;就是把同…

web学习笔记(二十七)PC端网页特效

目录 1.元素偏移量offset 1.1什么是offset 1.2offset系列常用属性 1.3offset总结 1.4offset 与 style 区别 2.元素可视区client 3.元素滚动scroll 4.总结 4.1三大系列总结 4.2 mouseenter 和mouseover的区别 1.元素偏移量offset 1.1什么是offset offset就是偏移量…

可视化图表:柱坐标系与对应图表详解

一、柱坐标系及其构成 柱状坐标系是一种常见的可视化图表坐标系&#xff0c;用于显示柱状图&#xff08;也称为条形图&#xff09;的数据。它由两个相互垂直的轴组成&#xff0c;一个是水平轴&#xff08;X轴&#xff09;&#xff0c;另一个是垂直轴&#xff08;Y轴&#xff0…

Linux编程3.4 进程-进程标识

1、相关函数 #include<unistd.h> #include<sys/types.h> pid_t getpid(void); 获得当前进程ID uid_t getuid(void); 获得当前进程的实际用户ID uit_t geteuid(void); 获得当前进程的有效用户ID git_t getgid(void); 获得当前进程的用户组ID pit_t getppid(…

深入解析Java中的异常处理机制

摘要&#xff1a; 异常处理是Java编程中不可或缺的一部分&#xff0c;它允许我们以优雅的方式处理程序运行时可能出现的问题。本文将深入探讨Java中的异常处理机制&#xff0c;包括异常类的层次结构、声明异常和处理异常的方法。通过两个实际的代码案例&#xff0c;我们将详细…

Mac版2024 CleanMyMac X 4.14.6 核心功能详解以及永久下载和激活入口

CleanMyMac 是 macOS 上久负盛名的系统清理工具&#xff0c;2018 年&#xff0c;里程碑式版本 CleanMyMac X 正式发布。不仅仅是命名上的变化&#xff0c;焕然一新的 UI、流畅的动画也让它显得更加精致。新增的系统优化、软件更新等功能&#xff0c;使得在日常使用 macOS 时有了…

【问题解决】| 关于vscode调试python文件 报错 且直接运行正常的诡异情况记录

关于python的debug报错&#xff0c;其实很奇怪 首先&#xff0c;对于工作区代码&#xff0c;我们可以通过CtrlShiftP 来切换Python解释器 这样的话&#xff0c;工作区的代码就不会报import error 而且这样的话是可以运行跑通的&#xff0c;但最抽象的一集来了&#xff0c;这…

排序——堆排序

本节继续复习排序算法。这次复习排序算法中的堆排序。 堆排序属于选择排序。 目录 什么是堆&#xff1f; 堆排序 堆排序的思想 堆排代码 向下调整算法 堆排整体 什么是堆&#xff1f; 在复习堆排序之前&#xff0c; 首先我们需要回顾一下什么是堆 。 堆的本质其实是一个数…

Linux文件描述符剖析

文章目录 文件描述符文件描述符分配规则重定向软硬链接软链接&#xff08;Symbolic Link&#xff09;&#xff1a;硬链接&#xff08;Hard Link&#xff09;&#xff1a; 文件描述符 文件描述符&#xff08;File Descriptor&#xff09;是一个非负整数&#xff0c;用于标识打开…

智能控制:物联网智能插座对接文档

介绍 一开始买的某米的插座&#xff0c;但是好像接口不开放&#xff0c;所以找到了这个插座&#xff0c;然后自己开发了下&#xff0c;用接口控制插座开关。wifi的连接方式&#xff0c;通电后一般几秒后就会连接上wifi&#xff0c;这个时候通过接口发送命令给他。 产品图片 通…

C++11线程同步之条件变量

C11线程同步之条件变量 condition_variable成员函数生产者和消费者模型 condition_variable_any成员函数生产者和消费者模型 条件变量是C11提供的另外一种用于 等待的同步机制&#xff0c;它能阻塞一个或多个线程&#xff0c;直到收到另外一个线程发出的通知或者超时时&#x…

ROS 2基础概念#5:执行器(Executor)| ROS 2学习笔记

在ROS 2中&#xff0c;Executor是一个核心概念&#xff0c;负责管理节点&#xff08;Node&#xff09;中的回调函数&#xff0c;如订阅消息的回调、服务请求的回调、定时器回调等。Executor决定了何时以及如何执行这些回调&#xff0c;从而在ROS 2系统中实现异步编程。 ROS 2 …