SpringBoot刪除清理垃圾文件的實(shí)現(xiàn)
一、什么是垃圾文件?
垃圾文件是指,在表單中用戶上傳了文件到服務(wù)器,但表單未被提交或保存,導(dǎo)致上傳的這個文件將不會被記錄到相關(guān)業(yè)務(wù)表中,再也不會被使用,成為了垃圾文件。時間久了服務(wù)器上會有越來越多無用的垃圾文件占用服務(wù)器存儲。
二、我處理垃圾文件的方式
對于垃圾文件,一般可配合緩存+事件監(jiān)聽進(jìn)行刪除;還有一種方式是定時任務(wù),定時掃表,掃描每一個使用文件資源的表字段,匯總成在使用的文件,再對比文件表,即可篩選出需要刪除的垃圾文件。本文中我將介紹兩種方式:
- Redis+事件監(jiān)聽器
- 定時掃表求差集刪除文件
方式一、Redis+事件監(jiān)聽器
當(dāng)文件通過上傳接口上傳到服務(wù)器時,應(yīng)在上傳接口將fileUrl存入Redis中并設(shè)置過期事件。
當(dāng)提交表單時,在ServiceImpl層方法中使用方法進(jìn)行驗(yàn)證,如果圖片在緩存中有,則說明圖片是在有效期內(nèi)提交的,直接從緩存中刪除對應(yīng)的key即可。這里我設(shè)計(jì)一個工具類AsyncFileHandler,checkValid()方法一次性可傳入多個字段進(jìn)行fileUrl校驗(yàn)。當(dāng)然這只能刪除新增時上傳不提交表單產(chǎn)生的垃圾文件,如果是更新時,舊文件被替換了,提交表單時也不會知道舊文件是誰,這時有朋友可能會說了:“前端文件組件在刪除文件時對應(yīng)的把文件也從服務(wù)器刪除不就好了嘛。”這么說其實(shí)也可以,但是針對富文本情況呢?富文本里上傳了圖片就沒法再通過上傳組件刪除了。不過別慌,我們可以在提交表單時查詢出舊數(shù)據(jù),讓新舊數(shù)據(jù)字段進(jìn)行對比,如果舊字段中的fileUrl未出現(xiàn)在新字段中,則應(yīng)刪除舊字段的fileUrl。當(dāng)然,在更新業(yè)務(wù)中也需要執(zhí)行checkValid()方法校驗(yàn)新上傳文件的有效性,再執(zhí)行compareField()方法檢測舊文件是否還被使用,不再使用則刪除。
那么,redis中過期的文件怎么處理呢?這需要定義一個過期事件監(jiān)聽器,當(dāng)key快過期時觸發(fā)監(jiān)聽器,我們通過監(jiān)聽器拿到redis key刪除未使用的垃圾文件即可。key保存的是fileUrl
1.將上傳的文件返回的fileUrl存入Redis并設(shè)置過期時間
String fileUrl = this.storageFile(engine, file, false); //上傳minio并返回fileUrl
String redisKey = "file:url:" + fileUrl;
cacheOperator.put(redisKey, fileUrl, 60 * 60 * 24); //將fileUrl存入Redis中并設(shè)置24小時過期
2.定義AsyncFileHandler類,方便檢測文件有效性和對比新文件刪除舊文件。
@Slf4j
@Service
@EnableAsync
@Component
public class AsyncFileHandler {
@Resource
private CommonCacheOperator cacheOperator;
@Resource
private DevFileApi devFileApi;
// URL正則表達(dá)式模式
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);
}
// 刪除在有效期內(nèi)提交表單的文件,不刪除的話后面會在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過期事件監(jiān)聽器
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Resource
private DevFileApi devFileApi;
/**
* 創(chuàng)建RedisKeyExpirationListener bean時注入 redisMessageListenerContainer
*
* @param redisMessageListenerContainer RedisConfig中配置的消息監(jiān)聽者容器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("監(jiān)聽到過期文件:" + expiredKey);
devFileApi.deleteFileByUrl(expiredKey);
}
}
}
方式二、定時掃表求差集刪除文件
一個系統(tǒng)中可能好幾個表中的多個字段使用文件資源,一個情況方式一也可以解決。假設(shè)有這樣一種情況:系統(tǒng)中的一個文件可能被多個表的字段共享,表中的字段存入的是同一個fileUrl,如果刪除這個文件可能會牽扯到好幾張表。這種情況就適合掃表求差集刪除文件了。首先找出全數(shù)據(jù)庫表中使用文件的字段,找出這些字段使用的fileUrl,肯定有重復(fù)的fileUrl,所以我們使用Set集合保存,多個fileUrl只保留一個即可,這個Set集合里存的就是我們數(shù)據(jù)庫中所有在使用的文件,非垃圾文件,那么直接在文件表中使用notIn查詢,查詢出不在使用的文件,也就是垃圾文件即可,然后刪除這些垃圾文件。直接上代碼:
/**
* 清理垃圾文件定時任務(wù)
*/
@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("在使用的文件數(shù)量:{}", 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("待清理的文件數(shù)量:{}", junkFileIds.size());
if (!junkFileIds.isEmpty()) {
// 執(zhí)行物理刪除垃圾文件
devFileService.deleteGarbageFiles(junkFileIds);
}
log.info("------------------------------------------------清理垃圾文件End------------------------------------------------\n");
}
}
注意:方式二刪除垃圾文件,一定不要遺漏需要過濾的數(shù)據(jù)表字段,否則會被誤刪除。
到此這篇關(guān)于SpringBoot刪除清理垃圾文件的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot刪除清理垃圾文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis中強(qiáng)大的resultMap功能介紹
這篇文章主要給大家介紹了關(guān)于Mybatis中強(qiáng)大的resultMap功能的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Mybatis具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
SpringBoot詳細(xì)講解視圖整合引擎thymeleaf
這篇文章主要分享了Spring Boot整合使用Thymeleaf,Thymeleaf是新一代的Java模板引擎,類似于Velocity、FreeMarker等傳統(tǒng)引擎,關(guān)于其更多相關(guān)內(nèi)容,需要的小伙伴可以參考一下2022-06-06
JAVA基本類型包裝類 BigDecimal BigInteger 的使用
Java 中預(yù)定義了八種基本數(shù)據(jù)類型,包括:byte,int,long,double,float,boolean,char,short,接下來文章小編將向大家介紹其中幾個類型的內(nèi)容,需要的朋友可以參考下文章2021-09-09
解決SpringBoot2.1.0+RocketMQ版本沖突問題
這篇文章主要介紹了解決SpringBoot2.1.0+RocketMQ版本沖突問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06

