乾貨!java檔案上傳判重姿勢淺談
一、場景:檔案上傳,使用者極有可能上傳重複檔案,內容完全一致。如果對上傳的檔案未做任何處理,對於檔案儲存系統來說將是災難,大量重複的資料,如果允許上傳大檔案,那麼對於儲存資源將是巨大的浪費。對於重複的檔案,只需要複製相應的訪問地址即可,原始檔可無需上傳,既減輕了網路頻寬壓力,也減少了儲存容量的壓力。
二、應對:
1、通過檔名判重。非特殊情況下,不會採用這種方案,理由跟人同名一樣,檔名很容易重複,隨著使用者上升,概率會變大。採用此方案極易導致不能達到判重的目的。
2、讀取檔案頭加部分內容。這種方案可以解決百分之五十的問題,缺點是隨著量的上升,重複的概率依然存在。
3、讀取檔案內容,進行hash計算,通常情況下,這種方案比較可靠,出現誤判的概率低。一些分散式檔案系統,如fastdfs等也是採取hash的方式進行檔案判重。
三、方案
開發語言:javaJDK1.8IDE:eclipse
機器配置:i5雙核記憶體4G 64位
四、程式碼實現
1、org.apache.commons.codec.digest.DigestUtils
@Test public void test1() String path = "your file path"; try { long begin = System.currentTimeMillis(); String md5 = DigestUtils.md5Hex(new FileInputStream(path)); long end = System.currentTimeMillis(); System.out.println(md5); System.out.println("time:" + ((end - begin) / 1000) + "s"); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
2、自定義緩衝區實現
@Test public void test2() { String path ="file path"; long begin = System.currentTimeMillis(); BigInteger bi = null; try { byte[] buffer = new byte[8192 * 10]; int len = 0; MessageDigest md = MessageDigest.getInstance("MD5"); File f = new File(path); FileInputStream fis = new FileInputStream(f); while ((len = fis.read(buffer)) != -1) { md.update(buffer, 0, len); } fis.close(); byte[] b = md.digest(); bi = new BigInteger(1, b); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } String md5 = bi.toString(16); long end = System.currentTimeMillis(); System.out.println(md5); System.out.println("time:" + ((end - begin) / 1000) + "s"); }
3、com.twmacinta.util.MD5
@Test
public void test3() {
String path ="file path";
long begin = System.currentTimeMillis();
try {
String md5 = MD5.asHex(MD5.getHash(new File(path)));
long end = System.currentTimeMillis();
System.out.println(md5);
System.out.println("time:" + ((end - begin) / 1000) + "s");
} catch (IOException e) {
e.printStackTrace();
}
}
4、NIO讀取
public class MD5FileUtil {
/**
* 預設的密碼字串組合,用來將位元組轉換成 16 進製表示的字元,apache校 驗下載的檔案的正確性用的就是預設的這個組合
*/
protected static char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
'f' };
protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
/**
* 生成檔案的md5校驗值
* @param file 檔案路徑
* @return MD5碼返回
* @throws IOException
*/
public static String getFileMD5(File file) throws IOException {
String encrStr = "";
// 讀取檔案
FileInputStream fis = new FileInputStream(file);
// 當檔案<2G可以直接讀取
if (file.length() <= Integer.MAX_VALUE) {
encrStr = getMD5Lt2G(file, fis);
} else { // 當檔案>2G需要切割讀取
encrStr = getMD5Gt2G(fis);
}
fis.close();
return encrStr;
}
/**
* 小於2G檔案
*
* @param fis 檔案輸入流
* @return
* @throws IOException
*/
public static String getMD5Lt2G(File file, FileInputStream fis) throws IOException {
// 加密碼
String encrStr = "";
FileChannel ch = fis.getChannel();
MappedByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
messagedigest.update(byteBuffer);
encrStr = bufferToHex(messagedigest.digest());
return encrStr;
}
/**
* 超過2G檔案的md5演算法
*
* @param fileName
* @param InputStream
* @return
* @throws Exception
*/
public static String getMD5Gt2G(InputStream fis) throws IOException {
// 自定義檔案塊讀寫大小,一般為4M,對於小檔案多的情況可以降低
byte[] buffer = new byte[1024 * 1024 * 4];
int numRead = 0;
while ((numRead = fis.read(buffer)) > 0) {
messagedigest.update(buffer, 0, numRead);
}
return bufferToHex(messagedigest.digest());
}
/**
*
* @param bt 檔案位元組流
* @param stringbuffer 檔案快取
*/
private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
// 取位元組中高 4 位的數字轉換, >>> 為邏輯右移,將符號位一起右移,此處未發現兩種符號有何不同
char c0 = hexDigits[(bt & 0xf0) >> 4];
// 取位元組中低 4 位的數字轉換
char c1 = hexDigits[bt & 0xf];
stringbuffer.append(c0);
stringbuffer.append(c1);
}
private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}
private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}
/**
* 判斷字串的md5校驗碼是否與一個已知的md5碼相匹配
* @param password 要校驗的字串
* @param md5PwdStr 已知的md5校驗碼
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}
/**
* 生成字串的md5校驗值
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}
/**
* 生成位元組流的md5校驗值
* @param s
* @return
*/
public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}
public static void main(String[] args) throws IOException {
String path ="path";
File big = new File(path);
long begin = System.currentTimeMillis();
String md5 = getFileMD5(big);
long end = System.currentTimeMillis();
System.out.println("md5:" + md5);
System.out.println("time " + (end - begin));
System.out.println("time:" + ((end - begin) / 1000) + "s");
}
}
五、測試結果
方式/時間 | 304KB | 31.2MB | 69.5MB | 600MB | 3.09G |
apache | 37ms | 489ms | 1121ms | 8987ms | 45699ms |
緩衝區 | 4ms | 134ms | 292ms | 9393ms | 45993ms |
md5 | 19ms | 173ms | 338ms | 9021ms | 48427ms |
nio去讀 | 22ms | 165ms | 320ms | 9815ms | 45347ms |
600M以下:緩衝區 > NIO > MD5 > Apache
600M以上:Apache>緩衝區>NIO>MD5
六、總結
以上資料取樣比較分散,可以採用均勻分佈樣本測試的結果可能更加特近真實效果,也可能得出一個轉折點,可根據不同的資料量採用不同的生成模式,其效率有一定差別:
有興趣的朋友私下可以進行多次測試,可得出更真實的結果
資料以小檔案為主的,使用緩衝區生成MD5的方式效率更高,而到了G級別的檔案,採用apache的生成方式更高效。上述結果僅供參考,實際情況下請各位根據需要採用不同的生成方式。如有更高效的生成方式,歡迎交流。