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
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...