業務需求
產品經理:小明啊,我們需要做一個附件上傳的需求,內容可能是圖片、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 進行文件上傳。
拓展閱讀
小結
文件上傳是非常常見的業務需求,上傳的性能問題是肯定要考慮和優化的一個問題。
上面的幾種方法可以靈活的組合使用,結合自己的業務進行更好的實踐。
希望本文對你有所幫助,如果喜歡,歡迎點贊收藏轉發一波。
我是老馬,期待與你的下次重逢。