提升檔案上傳效能的 4 種方式,你會嗎?

老馬嘯西風發表於2022-02-26

業務需求

產品經理:小明啊,我們需要做一個附件上傳的需求,內容可能是圖片、pdf 或者視訊。

小明:可以實現的,不過要限制下檔案大小。最好別超過 30MB,太大了上傳比較慢,伺服器壓力也大。

產品經理:溝通下來,視訊是一定要的。就限制 50MB 以下吧。

小明:可以。

A FEW DAYS LATER

測試同學:這檔案上傳也太慢了吧,我試了一個 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 進行檔案上傳。

擴充閱讀

提升檔案上傳效能的 4 種方式,你會嗎?

非同步查詢轉同步的 7 種實現方式

java壓縮歸檔演算法框架工具 compress

小結

檔案上傳是非常常見的業務需求,上傳的效能問題是肯定要考慮和優化的一個問題。

上面的幾種方法可以靈活的組合使用,結合自己的業務進行更好的實踐。

希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。

我是老馬,期待與你的下次重逢。

相關文章