業務需求
產品經理:小明啊,我們需要做一個附件上傳的需求,內容可能是圖片、pdf 或者視訊。
小明:可以實現的,不過要限制下檔案大小。最好別超過 30MB,太大了上傳比較慢,伺服器壓力也大。
產品經理:溝通下來,視訊是一定要的。就限制 50MB 以下吧。
小明:可以。
測試同學:這檔案上傳也太慢了吧,我試了一個 50mb 的檔案,花了一分鐘。
小明:whats up,這麼慢。
產品經理:不行,你這太慢了, 想辦法優化下。
優化之路
問題定位
整體的檔案上傳呼叫鏈路如下圖:
小明發現前端開始上傳,到請求到後端就花費了近 30 秒,應該是瀏覽器解析檔案導致的慢。
後端服務請求檔案服務也比較慢。
解決方案
小明:檔案服務有非同步介面嗎?
檔案服務:暫時沒有。
小明:這個上傳確實很慢,有優化建議嗎?
檔案服務:沒有,看了下就是這麼慢。
小明:……
最後小明還是決定把後端的同步返回,調整為非同步返回,降低使用者的等待時間。
把後端的實現調整了一番適應業務,前端呼叫後獲取非同步返回標識,後端根據標識查詢檔案服務同步返回的結果。
缺點也很明顯,非同步上傳失敗,使用者是不知道的。
不過礙於時間原因,也就是能權衡利弊,暫時上線了。
最近小明有些時間,於是就想著自己實現一個檔案服務。
檔案服務
礙於檔案服務的功能非常原始,小明就想著自己實現一個,從以下幾個方面優化:
(1)壓縮
(2)非同步
(3)秒傳
(4)併發
(5)直連
壓縮
日常開發中,儘可能和產品溝通清楚,讓使用者上傳/下載壓縮包檔案。
因為網路傳輸是非常耗時的。
壓縮檔案還有一個好處就是節約儲存空間,當然,一般我們不用考慮這個成本。
優點:實現簡單,效果拔群。
缺點:需要結合業務,並且說服產品。如果產品希望圖片預覽,視訊播放,壓縮就不太適用。
非同步
對於比較耗時的操作,我們會自然的想到非同步執行,降低使用者同步等待的時間。
服務端接收到檔案內容後,返回一個請求標識,非同步執行處理邏輯。
那如何獲取執行結果呢?
一般有 2 種常見方案:
(1)提供結果查詢介面
相對簡單,但是可能會有無效查詢。
(2)提供非同步結果回撥功能
實現比較麻煩,可以第一時間獲取執行結果。
秒傳
小夥伴們應該都用過雲盤,雲盤有時候上傳檔案,非常大的檔案,卻可以瞬間上傳完成。
這是如何實現的呢?
每一個檔案內容,都對應唯一的檔案雜湊值。
我們可以在上傳之前,查詢該雜湊值是否存在,如果已經存在,則直接增加一個引用即可,跳過了檔案傳輸的環節。
當然,這個只在你的使用者檔案資料量很大,且有一定重複率的時候優勢才能體現出來。
虛擬碼如下:
public FileUploadResponse uploadByHash(final String fileName,
final String fileBase64) {
FileUploadResponse response = new FileUploadResponse();
//判斷檔案是否存在
String fileHash = Md5Util.md5(fileBase64);
FileInfoExistsResponse fileInfoExistsResponse = fileInfoExists(fileHash);
if (!RespCodeConst.SUCCESS.equals(fileInfoExistsResponse.getRespCode())) {
response.setRespCode(fileInfoExistsResponse.getRespCode());
response.setRespMessage(fileInfoExistsResponse.getRespMessage());
return response;
}
Boolean exists = fileInfoExistsResponse.getExists();
FileUploadByHashRequest request = new FileUploadByHashRequest();
request.setFileName(fileName);
request.setFileHash(fileHash);
request.setAsyncFlag(asyncFlag);
// 檔案不存在再上傳內容
if (!Boolean.TRUE.equals(exists)) {
request.setFileBase64(fileBase64);
}
// 呼叫服務端
return fillAndCallServer(request, "api/file/uploadByHash", FileUploadResponse.class);
}
併發
另一種方式就是對一個比較大的檔案進行切分。
比如 100MB 的檔案,切成 10 個子檔案,然後併發上傳。一個檔案對應唯一的批次號。
下載的時候,根據批次號,併發下載檔案,拼接成一個完整的檔案。
虛擬碼如下:
public FileUploadResponse concurrentUpload(final String fileName,
final String fileBase64) {
// 首先進行分段
int limitSize = fileBase64.length() / 10;
final List<String> segments = StringUtil.splitByLength(fileBase64, limitSize);
// 併發上傳
int size = segments.size();
final ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
final CountDownLatch lock = new CountDownLatch(size);
for(int i = 0; i < segments.size(); i++) {
final int index = i;
Thread t = new Thread() {
public void run() {
// 併發上傳
// countDown
lock.countDown();
}
};
t.start();
}
// 等待完成
lock.await();
// 針對上傳後的資訊處理
}
直連
當然,還有一種策略就是客戶端直接訪問服務端,跳過後端服務。
當然,這個前提要求檔案服務必須提供 HTTP 檔案上傳介面。
還需要考慮安全問題,最好是前端呼叫後端,獲取授權 token,然後攜帶 token 進行檔案上傳。
擴充閱讀
小結
檔案上傳是非常常見的業務需求,上傳的效能問題是肯定要考慮和優化的一個問題。
上面的幾種方法可以靈活的組合使用,結合自己的業務進行更好的實踐。
希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。
我是老馬,期待與你的下次重逢。