FastDFS併發問題的排查經歷

来源:https://www.cnblogs.com/heavenTang/archive/2023/03/28/17266440.html
-Advertisement-
Play Games

附件用的fastdf上傳和下載的, 本地開發時就沒考慮過多文件上傳就會有併發的問題,比如多個只上傳成功了一個或者上傳了但是文檔內容缺失了,變成0位元組。 呵。。都是一次難忘的經歷。 經過本地模擬大批量的上傳下載, 發現fastdf是在啟動時就初始化了tracker和stroge, 每次調用過他的介面後 ...


附件用的fastdf上傳和下載的, 本地開發時就沒考慮過多文件上傳就會有併發的問題,比如多個只上傳成功了一個或者上傳了但是文檔內容缺失了,變成0位元組。

呵。。都是一次難忘的經歷。

經過本地模擬大批量的上傳下載, 發現fastdf是在啟動時就初始化了tracker和stroge, 每次調用過他的介面後都會關閉連接, 這樣就導致上傳的不完整或者不成功。也是後面找的博客看到的,非常感謝這篇文章。https://blog.csdn.net/AFSGEFEGH/article/details/109034532?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-3-109034532-blog-114929991.235^v27^pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-3-109034532-blog-114929991.235^v27^pc_relevant_multi_platform_whitelistv3&utm_relevant_index=6

記得方法上面加上synchronized

點擊查看代碼

	@RequestMapping(value = "/batchDownloadForThesisCheck2", method = RequestMethod.POST)
	public synchronized void batchDownloadForThesisCheck2(@RequestBody List<FileInfoDto> fileInfoList)  {
		if(CollectionUtils.isEmpty(fileInfoList)){
			throw new EducationException("下載附件失敗");
		}
		List<FileInfoDto> fileInfoDtos = differentFileName(fileInfoList);
//		List<FileInfoDto> fileInfoDtos = new ArrayList<>();

//		for(int fi=0;fi<300;fi++){
//			FileInfoDto it = new FileInfoDto();
//			it.setFileName("20210115_小魚兒"+fi+"_jjlw.doc");
//			fileInfoDtos.add(it);
//		}
		String zipName = request.getParameter("zipName");
		if(StringUtils.isEmpty(zipName)) zipName = "批量下載";
		ZipOutputStream zipOS = null ;
		InputStream is = null;

		OutputStream os = null;
		// 計算百分值
		int index =1;
		int totalSize =CollectionUtils.isNotEmpty(fileInfoDtos) ? fileInfoDtos.size():1;


		try {
			response.setContentType("application/octet-stream; charset=UTF-8");
			response.setHeader("Access-Control-Expose-Headers", "fileName");
			response.setHeader("fileName", URLEncoder.encode(zipName, "UTF-8"));
			os = response.getOutputStream();
			zipOS = new ZipOutputStream(os);
			for (FileInfoDto info : fileInfoDtos) {
				// 機檢論文換名字,學號_姓名_jjlw命名
				String itemFileName= info.getFileName();
//				itemFileName = "S20020804005_陳明鑫_jjlw .docx";
				int secondShowIndex = Common.findNumber(itemFileName,"_",2);
				int firstShowIndex = Common.findNumber(itemFileName,"_",1);
				if("1".equals(info.getPaperToName())){
					// 文件格式1:學校代碼_學號_LW.doc 2:學號_姓名_jjlw
					itemFileName = "10356_"+itemFileName.substring(0,firstShowIndex)+"_LW"+itemFileName.substring(itemFileName.lastIndexOf("."),itemFileName.length());
				}else{
					itemFileName = itemFileName.substring(0,secondShowIndex)+"_jjlw"+itemFileName.substring(itemFileName.lastIndexOf("."),itemFileName.length());
				}
				logger.error("已下載學生:{} "  ,itemFileName);
				zipOS.putNextEntry(new ZipEntry(itemFileName));
				try{
					is = fastDFS.downloadFile(info.getFileId());
//					is = fastDFS.downloadFile("group1/M00/00/C0/wKgjdWQYEJ-AbefsAdOyZXrKanw028.doc");
					int len = 0;
					byte[] buffer = new byte[1024*8];
					while ((len = is.read(buffer)) != -1) {
						zipOS.write(buffer, 0, len);
					}
//					is.close();

					// 計算進度,向下取整
					double nowProcess = Math.floor((index*100)/totalSize);
					logger.error("已下載”{}",index);
//					createProcessDownFile(nowProcess,"已下載"+nowProcess+"%",info.getTimeId(),info.getUserId());
					index ++;
				}catch(Exception ignored){

				}
				zipOS.flush();
//				zipOS.closeEntry();
			}
		} catch (IOException e) {
//			createProcessDownFile(100d,"批量下載發生錯誤",fileInfoList.get(0).getTimeId(),fileInfoList.get(0).getUserId());
			logger.error("批量下載發生錯誤: " + e.getMessage(), e);
		} finally {
			try {
				if (zipOS != null) {
					zipOS.closeEntry();
					zipOS.close();
				}
				if (os != null) os.close();
//				createProcessDownFile(100d,"已下載100%",fileInfoList.get(0).getTimeId(),fileInfoList.get(0).getUserId());
				logger.warn("關閉機檢下載 :"  );
			} catch (IOException e) {
//				createProcessDownFile(100d,"批量下載發生錯誤,關閉文件流失敗",fileInfoList.get(0).getTimeId(),fileInfoList.get(0).getUserId());
				logger.warn("關閉文件流失敗, cause by :" + e.getMessage());

			}
//			finally {
//				createProcessDownFile(100d,"已下載100%",fileInfoList.get(0).getTimeId(),fileInfoList.get(0).getUserId());
//				logger.warn("關閉機檢下載 :"  );
//			}

		}
	}

下麵是封裝的上傳FastDFS

點擊查看代碼
package fastdfs.config;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.annotation.PostConstruct;

import org.apache.commons.lang.StringUtils;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.ProtoCommon;
import org.csource.fastdfs.StorageClient1;
import org.csource.fastdfs.StorageServer;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerGroup;
import org.csource.fastdfs.TrackerServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;

import com.xx.commons.exception.EducationException;

import lombok.extern.slf4j.Slf4j;



/**
 * FastDfs文件系統工具類
 * 連接Fast
 * 上傳圖片 
 * 返回上傳之後的路徑 用此路徑就能訪問此圖片
 *    group1/M00/00/01/wKjIgFWOYc6APpjAAAD-qk29i78248.jpg
 *
 */
@Slf4j
public class FastDFS {

    @Autowired
    private  FastDFSProperty fastDFSProperty;

    /**
     * 跟蹤器
     */
    private TrackerServer trackerServer;
    
    /**
     * 存儲器
     */
    private StorageServer storageServer;
    
    /**
     * 預設編碼
     */
    private static final String DEFAULT_ENCODING = "UTF-8";

    @PostConstruct
    public void init(){
        try {
            ClientGlobal.setG_charset(DEFAULT_ENCODING);
            ClientGlobal.setG_connect_timeout(fastDFSProperty.getConnect_timeout());
            ClientGlobal.setG_network_timeout(fastDFSProperty.getNetwork_timeout());
            ClientGlobal.setG_secret_key(fastDFSProperty.getSecret_key());
            ClientGlobal.setG_tracker_http_port(fastDFSProperty.getTracker_http_port());

            String tracker_server = fastDFSProperty.getTracker_server();
            InetSocketAddress isadd = new InetSocketAddress(
                    tracker_server.substring(0, tracker_server.indexOf(':')), 
                    Integer.parseInt(tracker_server.substring(tracker_server.indexOf(':') + 1, tracker_server.length())));
            InetSocketAddress[] tracker_servers  = {isadd};
            ClientGlobal.setG_tracker_group(new TrackerGroup(tracker_servers));

            TrackerClient trackerClient = new TrackerClient(ClientGlobal.g_tracker_group);
            trackerServer = trackerClient.getConnection();
            if (trackerServer == null) {
                throw new EducationException("getConnection return null");
            }
            storageServer = trackerClient.getStoreStorage(trackerServer);
            if (storageServer == null) {
                throw new EducationException("getStoreStorage return null");
            }
            ProtoCommon.activeTest(storageServer.getSocket());
        } catch (Exception e) {
            throw new EducationException("初始化 fastdfs 配置失敗", e);
        }
    }

    /**
     * 
     * @param file
     *            文件
     * @param fileName
     *            文件名
     * @return 返回Null則為失敗
     */
    public String uploadFile(File file, String fileName) {
        InputStream fis = null;
        try {
            NameValuePair[] meta_list = null; 
            fis = Files.newInputStream(file.toPath());
            byte[] file_buff = new byte[1024];
            int len = fis.available();
            file_buff = new byte[len];
            while (fis.read(file_buff) > 0) {
                break;
            }
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
            String fileid = storageClient1.upload_file1(file_buff, getFileExt(fileName), meta_list);
            return fileid;
        } catch (Exception ex) {
            throw new EducationException("上傳文件錯誤",ex);
        } finally{
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    log.error("Close {} InputStream failed", fis);
                }
            }
        }
    }

    /**
     * 上傳文件
     * @param bytes
     * @param name
     * @param size
     * @return
     */
    public String uploadFile(byte[] bytes, String name, Long size) {
        try {
            //擴展名
            String ext = name.substring(name.lastIndexOf('.')+1);
            NameValuePair[] meta_list = new NameValuePair[3];
            meta_list[0] = new NameValuePair("filename",name);
            meta_list[1] = new NameValuePair("fileext",ext);
            meta_list[2] = new NameValuePair("filesize",String.valueOf(size));
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
            return storageClient1.upload_file1(bytes, ext, meta_list);
        } catch (Exception ex) {
            throw new EducationException("上傳文件錯誤",ex);
        }
    }

    /**
     * 根據組名和遠程文件名來刪除一個文件
     * 
     * @param groupName
     *            例如 "group1" 如果不指定該值,預設為group1
     * @param fileName
     *            例如"M00/00/00/wKgxgk5HbLvfP86RAAAAChd9X1Y736.jpg"
     * @return 0為成功,非0為失敗,具體為錯誤代碼
     */
    public int deleteFile(String groupName, String fileName) {
        try {
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
            return storageClient1.delete_file(StringUtils.isBlank(groupName)? "group1" : groupName, fileName);
        } catch (Exception ex) {
            throw new EducationException("return null",ex);
        }
    }

    /**
     * 根據fileId來刪除一個文件(我們現在用的就是這樣的方式,上傳文件時直接將fileId保存在了資料庫中)
     * 
     * @param fileId
     *            file_id源碼中的解釋file_id the file id(including group name and filename);例如 group1/M00/00/00/ooYBAFM6MpmAHM91AAAEgdpiRC0012.xml
     * @return 0為成功,非0為失敗,具體為錯誤代碼
     */
    public int deleteFile(String fileId) {
        try {
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
            return storageClient1.delete_file1(fileId);
        } catch (Exception ex) {
            throw new EducationException("刪除文件錯誤",ex);
        }
    }

    /**
     * 修改一個已經存在的文件
     * 
     * @param oldFileId
     *            原來舊文件的fileId, file_id源碼中的解釋file_id the file id(including group name and filename);例如 group1/M00/00/00/ooYBAFM6MpmAHM91AAAEgdpiRC0012.xml
     * @param file
     *            新文件
     * @param filePath
     *            新文件路徑
     * @return 返回空則為失敗
     */
    public String modifyFile(String oldFileId, File file, String filePath) {
        String fileid = null;
        try {
            // 先上傳
            fileid = uploadFile(file, filePath);
            if (fileid == null) {
                return null;
            }
            // 再刪除
            int delResult = deleteFile(oldFileId);
            if (delResult != 0) {
                return null;
            }
        } catch (Exception ex) {
            throw new EducationException("修改一個已經存在的文件錯誤",ex);
        }
        return fileid;
    }

    /**
     * 文件下載
     * 
     * @param fileId
     * @return 返回一個流
     */
    public InputStream downloadFile(String fileId) {
        try {
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
            byte[] bytes = storageClient1.download_file1(fileId);
            return new ByteArrayInputStream(bytes);
        } catch (Exception ex) {
            throw new EducationException("文件下載錯誤",ex);
        }
    }

    /**
     * 
     * @param fileId
     * @return 返回一個位元組數組
     */
    public byte[] downloadFileToByte(String fileId) {
        try {
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
            return storageClient1.download_file1(fileId);
        } catch (Exception ex) {
            throw new EducationException("文件下載錯誤",ex);
        }
    }

    /**  
     * 批量文件下載,在map中給出文件名:fileName, 文件對應路徑:filePath;   
     * 方法返回  由所有文件生成的壓縮包ZIP   
     * @param fileList 在map中給出文件名:fileName, 文件對應路徑:filePath;  
     * @return 返回一個流  
     */  
    public InputStream downloadFile(List<Map<String, String>> fileList){  
        if(CollectionUtils.isEmpty(fileList)){  
            throw new EducationException("文件下載錯誤,fileList為空!");  
        }  
        Date date = new Date();  
        long timeStr = date.getTime();  
        
        StorageClient1 storageClient1 = new StorageClient1(trackerServer, storageServer);
        
        try(ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(Paths.get(".", timeStr+".zip")));) {  
              
            ZipEntry entry;  
            int count, bufferLen = 1024;    
            byte data[] = new byte[bufferLen];  
            for(Map<String, String> map : fileList){  
                entry = new ZipEntry(map.get("fileName"));  
                  
                zos.putNextEntry(entry);  
                byte[] bytes = storageClient1.download_file1(map.get("filePath"));  
                try(BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(bytes));){  
                    while ((count = bis.read(data, 0, bufferLen)) != -1) {    
                        zos.write(data, 0, count);    
                    }    
                    zos.closeEntry();  
                }  
            }  
            return Files.newInputStream(Paths.get(".", timeStr+".zip"));  
        } catch (Exception ex) {  
            throw new EducationException("文件下載錯誤", ex);  
        }  
    } 

    /**
     * 獲取文件尾碼名(不帶點).
     * 
     * @return 如:"jpg" or "".
     */
    private  String getFileExt(String fileName) {
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return "";
        } else {
            return fileName.substring(fileName.lastIndexOf('.') + 1); // 不帶最後的點
        }
    }
}

fastdf源碼中storageServer每次用完都會關閉

image


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題1 問題原因:在數據源配置類中沒有創建事務管理 在數據源配置類中添加好事務管理器的Bean即可 問題2 其實出現這個問題實質就是mapper介面和mapper.xml文件沒有映射起來。 常見的錯誤如下: 1.mapper.xml中的namespace和實際的mapper文件不一致 這個問題其實很 ...
  • order by是怎麼工作的? 在你開發應用的時候,一定會經常碰到需要根據指定的欄位排序來顯示結果的需求。還是以我們前面舉例用過的市民表為例,假設你要查詢城市是“杭州”的所有人名字,並且按照姓名排序返回前 1000 個人的姓名、年齡。 假設這個表的部分定義是這樣的: CREATE TABLE `t` ...
  • Spring Boot 應用,在啟動的時候,如果想做一些事情,比如預先載入並緩存某些數據,讀取某些配置等等。總而言之,做一些初始化的操作時,那麼 Spring Boot 就提供了兩個介面幫助我們實現。 ...
  • L2-001 緊急救援 分數 25 作為一個城市的應急救援隊伍的負責人,你有一張特殊的全國地圖。在地圖上顯示有多個分散的城市和一些連接城市的快速道路。每個城市的救援隊數量和每一條連接兩個城市的快速道路長度都標在地圖上。當其他城市有緊急求助電話給你的時候,你的任務是帶領你的救援隊儘快趕往事發地,同時, ...
  • 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇介紹 VLD 配置文件中配置項 ReportFile 的使用方法。 ...
  • 項目練習01 1.項目介紹 這是一個簡單的項目練習,用於掌握新學習的SpringBoot技術。 項目操作界面 ● 技術棧 Vue3+ElementPlus+Axios+MyBatisPlus+SpringBoot 前後端分離 前後端分離開發,前端主體框架 Vue3 + 後端基礎框架 SpringBo ...
  • 什麼是Base64 Base64編碼是將字元串以每3個8比特(bit)的位元組子序列拆分成4個6比特(bit)的位元組(6比特有效位元組,最左邊兩個永遠為0,其實也是8比特的位元組)子序列,再將得到的子序列查找Base64的編碼索引表,得到對應的字元拼接成新的字元串的一種編碼方式。 每個3位8比特數據拆分成 ...
  • 藍橋杯【答疑】 題目描述 分析 這是一個貪心演算法,要所得的時刻之和最小,而且下一個同學需要等上一個同學結束以後才能進行,因此需要對所耗總時間進行有小到大的排序,總時間相同的同學則對前兩步時間之和有小到大進行排序,最後算出時間之和即可。 代碼 import java.util.Arrays; impo ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...