SpringBoot删除清理垃圾文件的实现

 更新时间:2025年10月30日 11:10:01   作者:CyberShen  
本文主要介绍了SpringBoot删除清理垃圾文件的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、什么是垃圾文件?

垃圾文件是指,在表单中用户上传了文件到服务器,但表单未被提交或保存,导致上传的这个文件将不会被记录到相关业务表中,再也不会被使用,成为了垃圾文件。时间久了服务器上会有越来越多无用的垃圾文件占用服务器存储。

二、我处理垃圾文件的方式

对于垃圾文件,一般可配合缓存+事件监听进行删除;还有一种方式是定时任务,定时扫表,扫描每一个使用文件资源的表字段,汇总成在使用的文件,再对比文件表,即可筛选出需要删除的垃圾文件。本文中我将介绍两种方式:

  • Redis+事件监听器
  • 定时扫表求差集删除文件

方式一、Redis+事件监听器

当文件通过上传接口上传到服务器时,应在上传接口将fileUrl存入Redis中并设置过期事件。

当提交表单时,在ServiceImpl层方法中使用方法进行验证,如果图片在缓存中有,则说明图片是在有效期内提交的,直接从缓存中删除对应的key即可。这里我设计一个工具类AsyncFileHandlercheckValid()方法一次性可传入多个字段进行fileUrl校验。当然这只能删除新增时上传不提交表单产生的垃圾文件,如果是更新时,旧文件被替换了,提交表单时也不会知道旧文件是谁,这时有朋友可能会说了:“前端文件组件在删除文件时对应的把文件也从服务器删除不就好了嘛。”这么说其实也可以,但是针对富文本情况呢?富文本里上传了图片就没法再通过上传组件删除了。不过别慌,我们可以在提交表单时查询出旧数据,让新旧数据字段进行对比,如果旧字段中的fileUrl未出现在新字段中,则应删除旧字段的fileUrl。当然,在更新业务中也需要执行checkValid()方法校验新上传文件的有效性,再执行compareField()方法检测旧文件是否还被使用,不再使用则删除。

那么,redis中过期的文件怎么处理呢?这需要定义一个过期事件监听器,当key快过期时触发监听器,我们通过监听器拿到redis key删除未使用的垃圾文件即可。key保存的是fileUrl

1.将上传的文件返回的fileUrl存入Redis并设置过期时间

    String fileUrl = this.storageFile(engine, file, false);     //上传minio并返回fileUrl
    String redisKey = "file:url:" + fileUrl;                    
    cacheOperator.put(redisKey, fileUrl, 60 * 60 * 24);         //将fileUrl存入Redis中并设置24小时过期

2.定义AsyncFileHandler类,方便检测文件有效性和对比新文件删除旧文件。

@Slf4j
 @Service
 @EnableAsync
 @Component
 public class AsyncFileHandler {

     @Resource
     private CommonCacheOperator cacheOperator;

     @Resource
     private DevFileApi devFileApi;

     // URL正则表达式模式
     private static final Pattern URL_PATTERN = Pattern.compile("/(\\d+)\\.[a-zA-Z0-9]+");

     @Async
     public void checkValid(String... imgFields) throws IOException {
         for (String field : imgFields) {
             Matcher matcher = URL_PATTERN.matcher(field);
             Set<String> extractedUrls = new HashSet<>();
             while (matcher.find()) {
                 String fileUrl = matcher.group(1);
                 extractedUrls.add(fileUrl);
             }
             // 删除Redis中匹配的URL
             deleteMatchedUrlsFromRedis(extractedUrls);
         }
     }

     // 比较新旧记录字段中文件的差异,并删除未被使用的旧字段
     @Async
     public void compareField(String newFiled, String oldFiled){
         Matcher matcher1= URL_PATTERN.matcher(newFiled);
         Matcher matcher2= URL_PATTERN.matcher(oldFiled);
         Set<String> newFileUrls = new HashSet<>();
         Set<String> oldFileUrls = new HashSet<>();
         while (matcher1.find()) {
             String fileUrl = matcher1.group(1);
             newFileUrls.add(fileUrl);
         }
         while (matcher2.find()) {
             String fileUrl = matcher2.group(1);
             oldFileUrls.add(fileUrl);
         }
         Set<String> toDelFileUrls = oldFileUrls.stream()
                 .filter(element -> !newFileUrls.contains(element))
                 .collect(Collectors.toSet());
         // 删除不再使用的文件
         deleteDiffFileUrls(toDelFileUrls);
     }
     
     // 删除在有效期内提交表单的文件,不删除的话后面会在redis过期事件中被删除。
     private void deleteMatchedUrlsFromRedis(Set<String> urls) {
         int deletedCount = 0;
         for (String url : urls) {
             String redisKey = "file:url:" + url;
             if (cacheOperator.get(url)!=null){
                 cacheOperator.remove(redisKey);
                 log.info("从Redis中删除已引用的文件URL: {}", url);
                 deletedCount++;
             }
         }
         log.info("共处理{}个文件URL,成功删除{}个Redis键", urls.size(), deletedCount);
     }
     
     // 删除新旧差异文件
     private void deleteDiffFileUrls(Set<String> urls) {
         for (String url : urls) {
             devFileApi.deleteFileByUrl(url);
         }
     }
}

3.定义Redis过期事件监听器

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

   @Resource
   private DevFileApi devFileApi;

   /**
    * 创建RedisKeyExpirationListener bean时注入 redisMessageListenerContainer
    *
    * @param redisMessageListenerContainer RedisConfig中配置的消息监听者容器bean
    */
   public RedisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) {
       super(redisMessageListenerContainer);
   }

   @Override
   public void onMessage(Message message, byte[] pattern) {
       String channel = new String(message.getChannel()); // __keyevent@*__:expired
       String pa = new String(pattern); // __keyevent@*__:expired
       String expiredKey = message.toString();
       if (expiredKey.startsWith("Cache:file:url:")){
           System.out.println("监听到过期文件:" + expiredKey);
           devFileApi.deleteFileByUrl(expiredKey);
       }
   }
}

方式二、定时扫表求差集删除文件

一个系统中可能好几个表中的多个字段使用文件资源,一个情况方式一也可以解决。假设有这样一种情况:系统中的一个文件可能被多个表的字段共享,表中的字段存入的是同一个fileUrl,如果删除这个文件可能会牵扯到好几张表。这种情况就适合扫表求差集删除文件了。首先找出全数据库表中使用文件的字段,找出这些字段使用的fileUrl,肯定有重复的fileUrl,所以我们使用Set集合保存,多个fileUrl只保留一个即可,这个Set集合里存的就是我们数据库中所有在使用的文件,非垃圾文件,那么直接在文件表中使用notIn查询,查询出不在使用的文件,也就是垃圾文件即可,然后删除这些垃圾文件。直接上代码:

/**
 * 清理垃圾文件定时任务
 */
@Slf4j
@Component
public class ClearGarbageFileTaskRunner implements CommonTimerTaskRunner {

    @Resource
    private DevFileService devFileService;

    @Override
    public void action(String extJson) {
        // 哪个表哪个字段在使用文件资源
        Map<String, String[]> map = new HashMap<>();
        map.put("BIZ_CHILD_USER", new String[]{"AVATAR"});
        map.put("BIZ_COLLECT", new String[]{"ANALYSIS_VIDEO","ANALYSIS", "COVER", "OPTIONS", "CONTENT"});
        map.put("BIZ_QUESTION", new String[]{"ANALYSIS_VIDEO", "ANALYSIS","COVER", "OPTIONS", "CONTENT"});
        map.put("BIZ_WRONG", new String[]{"ANALYSIS_VIDEO","ANALYSIS", "COVER", "OPTIONS", "CONTENT"});
        map.put("BIZ_EXAM_RECORD", new String[]{"ANALYSIS_VIDEO","ANALYSIS", "COVER", "OPTIONS", "CONTENT"});
        map.put("BIZ_LECTURE", new String[]{"VIDEO", "INTRODUCE"});
        map.put("BIZ_NOTICE", new String[]{"IMAGE", "CONTENT"});

        // 全表中在使用的文件ID集合
        Set<String> allTableInUseFileIdSet = new HashSet<>();
        for (Map.Entry<String, String[]> entry : map.entrySet()) {
            Set<String> oneTableInUseFileIdSet = devFileService.getInUseFileIdList(entry.getKey(), entry.getValue());
            allTableInUseFileIdSet.addAll(oneTableInUseFileIdSet);
        }
        log.info("------------------------------------------------清理垃圾文件Start------------------------------------------------");
        log.info("在使用的文件数量:{}", allTableInUseFileIdSet.size());
        // 获取垃圾文件ID
        List<DevFileIdParam> junkFileIds = devFileService.list(new LambdaQueryWrapper<DevFile>().notIn(DevFile::getId, allTableInUseFileIdSet))
                .stream().map(devFile -> {
                    DevFileIdParam param = new DevFileIdParam();
                    param.setId(devFile.getId()); // 确保字段名称匹配
                    return param;
                }).toList();
        log.info("待清理的文件数量:{}", junkFileIds.size());
        if (!junkFileIds.isEmpty()) {
            // 执行物理删除垃圾文件
            devFileService.deleteGarbageFiles(junkFileIds);
        }
        log.info("------------------------------------------------清理垃圾文件End------------------------------------------------\n");
    }
}

注意:方式二删除垃圾文件,一定不要遗漏需要过滤的数据表字段,否则会被误删除。

到此这篇关于SpringBoot删除清理垃圾文件的实现的文章就介绍到这了,更多相关SpringBoot删除清理垃圾文件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis中强大的resultMap功能介绍

    Mybatis中强大的resultMap功能介绍

    这篇文章主要给大家介绍了关于Mybatis中强大的resultMap功能的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Mybatis具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-06-06
  • SpringBoot详细讲解视图整合引擎thymeleaf

    SpringBoot详细讲解视图整合引擎thymeleaf

    这篇文章主要分享了Spring Boot整合使用Thymeleaf,Thymeleaf是新一代的Java模板引擎,类似于Velocity、FreeMarker等传统引擎,关于其更多相关内容,需要的小伙伴可以参考一下
    2022-06-06
  • JAVA基本类型包装类 BigDecimal BigInteger 的使用

    JAVA基本类型包装类 BigDecimal BigInteger 的使用

    Java 中预定义了八种基本数据类型,包括:byte,int,long,double,float,boolean,char,short,接下来文章小编将向大家介绍其中几个类型的内容,需要的朋友可以参考下文章
    2021-09-09
  • spring boot simple类型cache使用详解

    spring boot simple类型cache使用详解

    这篇文章主要介绍了spring boot simple类型cache使用,这里用的不是 redis 的缓存,simple 的缓存默认用的是java的ConcurrentHashMap, 单纯的simple缓存,本文给大家介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • 解决springcloud Zuul丢失Cookie的问题

    解决springcloud Zuul丢失Cookie的问题

    这篇文章主要介绍了解决springcloud Zuul丢失Cookie的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • SpringBoot如何集成Netty

    SpringBoot如何集成Netty

    这篇文章主要介绍了SpringBoot如何集成Netty问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • JAVA中反射机制和模块化的深入讲解

    JAVA中反射机制和模块化的深入讲解

    很多刚学Java反射的同学可能对反射技术一头雾水,为什么要学习反射,学习反射有什么作用,下面这篇文章主要给大家介绍了关于JAVA中反射机制和模块化的相关资料,需要的朋友可以参考下
    2021-09-09
  • 浅谈Spring的两种配置容器

    浅谈Spring的两种配置容器

    这篇文章主要介绍了浅谈Spring的两种配置容器,介绍了其实现以及简单的实例,具有一定参考价值,需要的朋友可以了解下。
    2017-10-10
  • Java 通过API操作GraphQL

    Java 通过API操作GraphQL

    这篇文章主要介绍了Java 通过API操作GraphQL的方法,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下
    2021-05-05
  • 解决SpringBoot2.1.0+RocketMQ版本冲突问题

    解决SpringBoot2.1.0+RocketMQ版本冲突问题

    这篇文章主要介绍了解决SpringBoot2.1.0+RocketMQ版本冲突问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06

最新评论