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

news2024/11/18 5:46:08

文章目录

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

功能介绍

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

原始方案

使用分批处理,避免单次加载表中的所有数据,导致发现内存溢出,每次从备份文件表中查询出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倍左右

在这里插入图片描述

功能可用性测试

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在数据源中删除测试文件

在这里插入图片描述

成功清理了两个文件

在这里插入图片描述

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

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

buuctf EasyBypass --不会编程的崽

buu后边的题有些确实难&#xff0c;有些其实也没那么复杂。昨天做一道异或绕过的题&#xff0c;现在还没看懂QAQ 先来一题简单的吧。哎&#xff0c;随缘更新吧 <?phphighlight_file(__FILE__);$comm1 $_GET[comm1]; $comm2 $_GET[comm2];if(preg_match("/\|\|\\|\…

新规正式发布 | 百度深度参编《生成式人工智能服务安全基本要求》

2024年2月29日&#xff0c;全国网络安全标准化技术委员会&#xff08; TC260 &#xff09;正式发布《生成式人工智能服务安全基本要求》&#xff08;以下简称《基本要求》&#xff09;。《基本要求》规定了生成式人工智能服务在安全方面的基本要求&#xff0c;包括语料安全、模…

弱电综合布线:连接现代生活的纽带

在当今信息化快速发展的时代&#xff0c;弱电网络布线作为信息传输的重要基础设施&#xff0c;其作用日益凸显。它不仅保障了数据的高效流通&#xff0c;还确保了通信的稳定性。从商业大厦到教育机构&#xff0c;从政府机关到医院急救中心&#xff0c;再到我们居住的社区&#…

【开课】云贝教育2024年3月9日-PostgreSQL中级工程师PGCE认证培训开课啦!

课程介绍 根据学员建议和市场需求,规划和设计了《PostgreSQL CE 认证课程》,本课程以内部原理、实践实战为主&#xff0c;理论与实践相结合。课程包含PG 简介、安装使用、服务管理、体系结构等基础知识。同时结合一线实战案例&#xff0c; 面向 PG 数据库的日常维护管理、服务和…

如何远程访问电脑文件?

远程访问电脑文件是当今数字化时代中十分常见且实用的技术。它允许我们从任何地方的计算机或移动设备访问和操作我们的电脑中的文件。无论是远程工作、远程学习、远程协作还是方便地获得自己计算机上的重要文件&#xff0c;远程访问电脑文件都为我们提供了巨大的便利。 在远程访…

从 iPhone 15/15 Pro 恢复丢失数据的 3 种方法

毫无疑问&#xff0c; iPhone 15 是迄今为止最令人印象深刻的 iPhone 。另一方面&#xff0c;我们知道&#xff0c;设备上保存的数据无论多么可靠&#xff0c;在设备使用过程中都可能因各种原因而丢失。 由于这些设备的性质&#xff0c;您在使用 iPhone 15、iPhone 15 Pro 或 …

大语言模型系列-GPT-2

文章目录 前言一、GPT-2做的改进二、GPT-2的表现总结 前言 《Language Models are Unsupervised Multitask Learners&#xff0c;2019》 前文提到&#xff0c;GPT-1利用不同的模型结构微调初步解决了多任务学习的问题&#xff0c;但是仍然是预训练微调的形式&#xff0c;GPT-…

[密码学]Base64编码

一、相关指令 1. 查看工具版本号 base64 --version2. 对字符串加密 echo 字符串 | base64 echo "Hello base64" | base643. 对字符串解密 echo 字符串 |base64 -d echo "SGVsbG8gTGV0aWFuLVJTQQo" | base64 -d4. 对文件加密 base64 文件名 base64 tex…

【Vue 3】

v-model 作用&#xff1a;给表单元素使用&#xff0c;双向数据绑定---->可以快速获取或设置表单元素内容 是value属性和input事件的合写 数据变化--->视图自动更新试图变化--->数据自动更新 语法&#xff1a;v-model"变量" 数据变&#xff0c;视图跟着变…

【你也能从零基础学会网站开发】Web建站之HTML+CSS入门篇 常用HTML标签(2)

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 超级链接标…

遗传算法优化BP神经网络时间序列回归分析,ga-bp回归分析

目录 BP神经网络的原理 BP神经网络的定义 BP神经网络的基本结构 BP神经网络的神经元 BP神经网络的激活函数, BP神经网络的传递函数 遗传算法原理 遗传算法主要参数 遗传算法流程图 完整代码包含数据下载链接: 遗传算法优化BP神经网络时间序列回归分析,ga-bp回归分析(代码完…

实现的一个网页版的简易表白墙

实现的一个网页版的表白墙 实现效果 代码截图 相关代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><tit…

Java面试(4)之 Spring Bean生命周期过程

一, 整个加载的完整链路图 更详细的生命周期函数链路图(仅供参考) 二, Bean实例化的四种方式: 1, 无参构造器(默认且常用)6 2, 静态工厂方法方式(factory-method指定实例化的静态方法) 3, 实例工厂方法方式(factory-bean指定bean的name,factory-method指定实例化方法) 4, 实…

(黑马出品_04)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

&#xff08;黑马出品_04&#xff09;SpringCloudRabbitMQDockerRedis搜索分布式 微服务技术异步通信 今日目标1.初识MQ1.1.同步和异步通讯1.1.1.同步通讯1.1.2.异步通讯 1.2.技术对比 2.快速入门2.1.安装RabbitMQ2.1.1.单机部署(1).下载镜像方式…

Spark实战-基于Spark日志清洗与数据统计以及Zeppelin使用

Saprk-日志实战 一、用户行为日志 1.概念 用户每次访问网站时所有的行为日志(访问、浏览、搜索、点击)用户行为轨迹&#xff0c;流量日志2.原因 分析日志&#xff1a;网站页面访问量网站的粘性推荐3.生产渠道 (1)Nginx(2)Ajax4.日志内容 日志数据内容&#xff1a;1.访问的…

2024_01蓝桥杯STEMA 考试 Scratch 中级试卷解析​​​​​​​

2024_01蓝桥杯STEMA 考试 Scratch 中级试卷解析一、选择题第一题、运行下列哪段程序后,蜜蜂会向上移动?(C ) 第二题、运行以下程序,输入下列哪个数后,角色会说“未通过”?( D) A. 90 B. 85 C. 60 D. 58第三题、运行以下程序后,n 的值为(B )。 A. 17 B…

机器学习 | 使用CatBoost处理缺失值

数据是任何分析或机器学习的基础。然而&#xff0c;现实世界的数据集并不完美&#xff0c;它们经常包含缺失值&#xff0c;这可能导致任何算法的训练阶段出现错误。处理缺失值至关重要&#xff0c;因为它们可能会导致数据分析和机器学习模型中出现偏差或不准确的结果。处理缺失…

第5章 HSA内存模型

5.1 引言 在共享内存环境中&#xff0c;独立的控制线程可以竞相修改单个位置。为程序以可预测的方式运行&#xff0c;程序员必须用同步来控制这些竞争。 “内存一致性模型”或“内存模型”定义了并行代理之间通信的基本规则。当这些规则含糊不清地定义或者更糟的是完全不存在…

OpenHarmony教程指南—Ability的启动模式

介绍 本示例展示了在一个Stage模型中&#xff0c;实现standard、singleton、specified多种模式场景。 本实例参考开发指南 。 本实例需要使用aa工具 查看应用Ability 模式信息。 效果预览 使用说明 1、standard模式&#xff1a; 1&#xff09;进入首页&#xff0c;点击番茄…

Linux ubuntu 写c语言Hello world

文章目录 创建hello.c 文件进入hello.c 文件使用vim 编辑器进行编辑下载gcc 编辑器调用gcc 进行编译hello.c 创建hello.c 文件 touch hello.c进入hello.c 文件 vi hello.c使用vim 编辑器进行编辑 下载gcc 编辑器 sudo apt update sudo apt install gcc第一个语句是更新&am…