【文件增量备份系统】使用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,否则返回false
    while (rs.next()) {
        // 获取数据中的属性
        long fileId = rs.getLong("id");
        String sourceFilePath = rs.getString("source_file_path");
        String targetFilePath = rs.getString("target_file_path");

        // 所扫描的文件数量+1
        finishFileNum++;

        // 获取备份文件的路径
        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倍左右

在这里插入图片描述

功能可用性测试

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在数据源中删除测试文件

在这里插入图片描述

成功清理了两个文件

在这里插入图片描述

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

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多线程多进程

秋招面试的java八股文知识点补充以及iot 这里有一点阅读补充 线程和进程区别 什么是进程? 进程 (Process) 是计算机中的一个独立执行单元&#xff0c;是操作系统资源分配的基本单位。每个进程有各自独立的内存空间和资源&#xff0c;它们之间相互独立&#xff0c;相互之间…

长期异地就医备案有效期是多久?答记者问!

4、长期异地就医登记的有效期是多长&#xff1f; 答&#xff1a;异地长期就医登记长期有效。 如果您因个人原因需要变更长期居住地&#xff0c;只需提供相应的登记信息即可申请变更。 5、临时异地就医登记的有效期是多长时间&#xff1f; 答&#xff1a;临时异地就医登记包括…

ORACLE RAC数据库压力测试(swingbench)

------------------------------------------------------------------- 欢迎关注作者 墨天伦:潇湘秦的个人主页 - 墨天轮 CSDN:潇湘秦-CSDN博客 公众号:潇湘秦的DBA之路 ------------------------------------------------------------------- 为了验证跑在虚拟机上的or…

【C++庖丁解牛】模拟实现STL的string容器(最后附源码)

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.vs和g下string结构…

电机应用-步进电机进阶驱动

步进电机梯形加减速 什么是梯形加减速 假设该装置使用步进电机实现物体X的移动&#xff0c;系统要求从A点出发&#xff0c;到B点停止&#xff0c;移动的时间越短越好且系统稳定。 根据步进电机的特性&#xff0c;最大程度加大电机转速&#xff08;提高脉冲频率&#xff09;&a…

【脚本玩漆黑的魅影】全自动对战宫殿

文章目录 原理主要代码全部代码 原理 对战宫殿是让宠物自己打&#xff0c;不需要我们选技能&#xff0c;所以用来刷对战点数很合适。 需要准备三个主力。 主要是根据屏幕截图进行各种操作。 1&#xff0c;外面的对话&#xff0c;除了选自由级以外&#xff0c;其他都是直接点…

第十一个实验:数组和簇的混用,线性

实验内容: 输入5个元素的一维数组 显示每一个元素的值,索引和奇偶类型 第一步:新建项目 第二步:编程 创建一维数组,一共五个元素 ​​​ 选择数组索引部件 判断元素的奇偶性,把元素值,该元素索引和奇偶特性组成簇 复制4份,每一个元素一份

(day 2)JavaScript学习笔记(基础之变量、常量和注释)

概述 这是我的学习笔记&#xff0c;记录了JavaScript的学习过程&#xff0c;我是有一些Python基础的&#xff0c;因此在学习的过程中不自觉的把JavaScript的代码跟Python代码做对比&#xff0c;以便加深印象。我本人学习软件开发纯属个人兴趣&#xff0c;大学所学的专业也非软件…

MySQL技能树学习

MySQL三大范式&#xff1a; 第一范式主要是确保数据表中每个字段的值必须具有原子性&#xff0c;也就是说数据表中每个字段的值为不可再次拆分的最小数据单元。 第二范式是指在第一范式的基础上&#xff0c;确保数据表中除了主键之外的每个字段都必须依赖主键。 第三范式是在…

【设计模式】享元模式的使用场景及与其他共享技术的对比

文章目录 1.概述2.享元模式2.1.核心概念2.2.实现案例2.2.1.内部状态实现2.2.2.外部状态实现 2.3.更多场景 3.享元模式的一些对比3.1.与缓存的区别3.2.与池化技术的区别 4.总结 1.概述 享元模式&#xff08;Flyweight Pattern&#xff09;是一种非常常用的结构型设计模式&#…

实在TARS大模型斩获多项重磅大奖,AI领域实力认可

近日&#xff0c;实在智能TARS&#xff08;塔斯&#xff09;大模型凭借在多个垂直行业场景的优秀落地应用案例&#xff0c;以及AIGC领域的深耕和技术积累&#xff0c;荣获多项重磅大奖。 TARS大模型是是实在智能基于在自然语言处理&#xff08;NLP&#xff09;领域深厚的技术积…

MySQL常见的索引类型介绍

我将为您详细讲解 MySQL 中常见的索引类型&#xff0c;以及它们的使用场景、特点、区别和优势。索引是提高数据库查询性能的关键工具&#xff0c;它可以加速数据检索速度&#xff0c;减少服务器的负担。在 MySQL 中&#xff0c;索引类型主要包括 B-Tree 索引、哈希索引、全文索…

Linux进程概念僵尸进程孤儿进程

文章目录 一、什么是进程二、进程的状态三、Linux是如何做的&#xff1f;3.1 R状态3.2 S状态3.3 D状态3.4 T状态3.5 t状态3.6 X状态3.7 Z状态 四、僵尸进程4.1 僵尸进程危害 五、孤儿进程 一、什么是进程 对于进程理解来说&#xff0c;在Windows上是也可以观察到的&#xff0c…

Java线程的6种状态

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。 NEW&#xff1a;初始状态&#xff0c;线程被创建出来但没有被调用start()RUNNABLE&#xff1a;运行状态&#xff0c;线程被调用了start()等待运行的状态BLOCKED&#xff1a;阻塞状态&#xf…

新手如何快速上手学习单片机?

读者朋友能容我&#xff0c;不使博文负真心 新开专栏&#xff0c;期待与诸君共享精彩 个人主页&#xff1a;17_Kevin-CSDN博客 专栏&#xff1a;《单片机》 学习单片机是一个有趣且有挑战性的过程。单片机是一种微控制器&#xff0c;广泛应用于各种电子设备和嵌入式系统中。在这…

开源向量数据库介绍

在开源矢量数据库的世界里&#xff0c;有些名字因其性能、灵活性和健壮性而脱颖而出。 1. Milvus Milvus 由 Zilliz 推出&#xff0c;是一款高度可定制的开源矢量数据库&#xff0c;在处理大规模数据方面大放异彩。由于其出色的可扩展性&#xff0c;当你需要处理大量数据时&a…

Python对头发二维建模(考虑风力、重力)

目录 一、背景 二、代码 一、背景 数值方法被用于创建电影、游戏或其他媒体中的计算机图形。例如&#xff0c;生成“逼真”的烟雾、水或爆炸等动画。本文内容是对头发的模拟&#xff0c;要求考虑重力、风力的影响。 假设&#xff1a; 1、人的头部是一个半径为10厘米的球体。…

python学习28

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

llc的基波分析法

对于我们之前分析的 LLC等效谐振电路的分析&#xff0c;其实我们发现分析的并不是完整的方波输入&#xff0c;而是用正弦波来分的 那么为何用基波来分析呢&#xff0c;因为对于方波而言&#xff0c;根据傅里叶级数它是可以分解成基波、 1次、3次、5次.......等各种奇次谐波的入…

《ElementPlus 与 ElementUI 差异集合》el-input 和 el-button 属性 size 有变化

差异 element-ui el-input 和 el-button 中&#xff0c;属性size 值是 medium / small / minielement-plus el-input 和 el-button 中&#xff0c;属性size 值是 ‘large’ | ‘default’ | ‘small’&#xff1b; 如果你是自动升级&#xff0c;Vue3 系统会有如下警告“ el-b…