大文件上傳、斷點續傳、秒傳、beego、vue

大文件上傳

0、項目源碼地址

源碼地址 :https://github.com/zhuchangwu/large-file-upload

前端基於 vue-simple-uploader (感謝這個大佬)實現: https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md

vue-simple-uploader底層封裝了uploader.js : https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md

1、如何唯一標識一個文件?

文件的信息後端會存儲在mysql數據庫表中。

在上傳之前,前端通過 spark-md5.js 計算文件的md5值以此去唯一的標示一個文件。

spark-md5.js 地址:https://github.com/satazor/js-spark-md5

README.md中有spark-md5.js的使用demo,可以去看看。

2、斷點續傳是如何實現的?

斷點續傳可以實現這樣的功能,比如用戶上傳200M的文件,當用戶上傳完199M時,斷網了,有了斷點續傳的功能,我們允許RD再次上傳時,能從第199M的位置重新上傳。

實現原理:

實現斷點續傳的前提是,大文件切片上傳。然後前端得問後端哪些chunk曾經上傳過,讓前端跳過這些上傳過的chunk就好了。

前端的上傳器(uploader.js)在上傳時會先發送一個GET請求,這個請求不會攜帶任何chunk數據,作用就是向後端詢問哪些chunk曾經上傳過。 後端會將這些數據保存在mysql數據庫表中。比如按這種格式:1:2:3:5表示,曾經上傳過的分片有1,2,3,5。第四片沒有被上傳,前端會跳過1,2,3,5。 僅僅會將第四個chunk發送給後端。

3、秒傳是如何實現的?

秒傳實現的功能是:當RD重複上傳一份相同的文件時,除了第一次上傳會正常發送上傳請求后,其他的上傳都會跳過真正的上傳,直接显示秒成功。

實現方式:

後端存儲着當前文件的相關信息。為了實現秒傳,我們需要搞一個字段(isUploaded)表示當前md5對應的文件是否曾經上傳過。 後端在處理 前端的上傳器(uploader.js)發送的第一個GET請求時,會將這個字段發送給前端,比如 isUploaded = true。前端看到這個信息后,直接跳過上傳,显示上傳成功。

4、上傳暫停是如何實現的?

上傳的暫停:並不是去暫停一個已經發送出去的正在進行數據傳輸的http請求~

而是暫停發送起發送下一個http請求。

就我們的項目而言,因為我們的文件本來就是先切片,對於我們來說,暫停文件的上傳,本質上就是暫停發送下一個chunk。

5、前端上傳併發數是多少?

前端的uploader.js中默認會三條線程啟動併發上傳,前端會在同一時刻併發 發送3個chunk,後端就會相應的為每個請求開啟三個協程處理上傳的過來的chunk。

在我們的項目中,會將前端併發數調整成了1。原因如下:

因為考慮到了斷點續傳的實現,後端需要記錄下曾經上傳過哪些切片。(這個記錄在mysql的數據庫表中,以 ”1:2:3:4:5“ )這種格式記錄。

Mysql5.7默認的存儲引擎是innoDB,默認的隔離級別是RR。如果我們將前端的併發數調大,就會出現下面的異常情況:

1. goroutine1 獲取開啟事物,讀取當前上傳到記錄是 1:2 (未提交事物)
2. goroutine1 在現有的記錄上加上自己處理的分片3,並和現有的1:2拼接在一起成1:2:3 (未提交事物)
3. goroutine2 獲取開啟事物,(因為RR,所以它讀不到1:2:3)讀取當前上傳到記錄是 1:2 (未提交事物)
4. goroutine1 提交事物,將1:2:3寫回到mysql
5. goroutine2 在現有的記錄上加上自己處理的分片4,並和現有的1:2拼接在一起成1:2:4 (提交事物)

可以看到,如果前端併發上傳,後端就會出現分片丟失的問題。 故前端將併發數置為1。

6、單個chunk上傳失敗怎麼辦?

前端會重傳chunk?

由於網絡問題,或者時後端處理chunk時出現的其他未知的錯誤,會導致chunk上傳失敗。

uploaded.js 中有如下的配置項, 每次uploader.js 在上傳每一個切片實際上都是在發送一次post請求,後端根據這個post請求是會給前端一個狀態嗎。 uploader.js 就是根據這個狀態碼去判斷是失敗了還是成功了,如果失敗了就會重新發送這個上傳的請求。

那uploader.js是如何知道有哪些狀態嗎是它應該重傳chunk的標記呢? 看看下面uploader.js需要的options 就明白了,其中的permantErrors中配置的狀態碼標示:當遇到這個狀態碼時整個上傳直接失敗~

successStatuses中配置的狀態碼錶示chunk是上傳成功的~。 其他的狀態嗎uploader.js 就會任務chunk上傳的有問題,於是重新上傳~

        options: {
          target: 'http://localhost:8081/file/upload',
          maxChunkRetries: 3,
          permanentErrors:[502], // 永久性的上傳失敗~,會認為整個文件都上傳失敗了
          successStatuses:[200], // 當前chunk上傳成功后的狀態嗎
          ...
        }

7、超過重傳次數后,怎麼辦?

比如我們設置出錯后重傳的次數為3,那麼無論當前分片是第幾片,整個文件的上傳狀態被標記為false,這就意味着會終止所有的上傳。

肯定不會出現這種情況:chunk1重傳3次后失敗了,chunk2還能再去上傳,這樣的話數據肯定不一致了。

8、如何控制上傳多大的文件?

目前了解到nginx端的限制上單次上傳不能超過1M。

前端會對大文件進行切片突破nginx的限制。

        options: {
          target: 'http://localhost:8081/file/upload',
          chunkSize: 512000, // 單次上傳 512KB 
        }     

如果後續和nginx負責的同學達成一致,可以把這個值進行調整。前端可以後續將這個chunk的閾值加大。

9、如何保證上傳文件的百分百正確?

在上傳文件前,前端會計算出當前RD選擇的這個文件的 md5 值。

當後端檢測到所有的分片全部上傳完畢,這時會merge所有分片匯聚成單個文件。計算這個文件的md5 同 RD在前端提供的文件的md5值比對。 比對結果一致說明RD正確的完成了上傳。結果不一致,說明文件上傳失敗了~返回給前端任務失敗,提示RD重新上傳。

10、其他細節問題:

如何判斷文件上傳失敗了,給RD展示紅色?

如何控制上傳什麼類型的文件?

如何控制不能上傳空文件?

上面說過了,當 uploader.js 遇到了permanentErrors這種狀態碼時會認為文件上傳失敗了。

前端想在上傳失敗后,將進度條轉換成紅色,其實改一下CSS樣式就好了,問題就在於,根據什麼去修改?在哪裡去修改?

前端會將每一個file封裝成一個組件:如下圖中的files就是file的集合

整個的fileList會將會被渲染成下面這樣。

我們上傳的文件被vue-simple-uploader的作者封裝成一個file.vue組件,這個對象中會有個配置參數, 比如它會長下面這樣。

     options: {
        target: 'http://localhost:8081/file/upload',
        statusText: {
          success: '上傳成功',
          error: '上傳出錯,請重試',
          typeError: '暫不支持上傳您添加的文件格式',
          uploading: '上傳中',
          emptyError:'不能上傳空文件',
          paused: '請確認文件後點擊上傳',
          waiting: '等待中'
        }
      }
    },

我們將上面的配置添加給Uploader.js

      const uploader = new Uploader(this.options)

在file組件中有如下計算屬性的,分別是status和statusText

    computed: {
      // 計算出一個狀態信息
      status () {
        const isUploading = this.isUploading // 是否正在上傳
        const isComplete = this.isComplete // 是否已經上傳完成
        const isError = this.error // 是否出錯了
        const isTypeError = this.typeError // 是否出錯了
        const paused = this.paused // 是否暫停了
        const isEmpty = this.emptyError // 是否暫停了
        // 哪個屬性先不為空,就返回哪個屬性
        if (isComplete) {
          return 'success'
        } else if (isError) {
          return 'error'
        } else if (isUploading) {
          return 'uploading'
        } else if (isTypeError) {
          return 'typeError'
        } else if (isEmpty) {
          return 'emptyError'
        } else if (paused) {
          return 'paused'
        } else {
          return 'waiting'
        }
      },
      // 狀態文本提示信息
      statusText () {
        // 獲取到計算出的status屬性(相當於是個key,具體的值在下面的fileStatusText中獲取到)
        const status = this.status
        // 從file的uploader對象中獲取到 fileStatusText,也就是用自己定義的名字
        const fileStatusText = this.file.uploader.fileStatusText
        let txt = status
        if (typeof fileStatusText === 'function') {
          txt = fileStatusText(status, this.response)
        } else {
          txt = fileStatusText[status]
        }
        return txt || status
      },
    },

status綁定在html上

	<div class="uploader-file" :status="status">

對應的CSS樣式入下:

  .uploader-file[status="error"] .uploader-file-progress {
    background: #ffe0e0;
  }

綜上:有了上面代碼的編寫,我們可以直接像下面這樣控制就好了

  file.typeError = true // 表示文件的類型不符合我們的預期,不允許RD上傳
  file.error = true // 表示文件上傳失敗了
  file.emptyError = true // 表示文件為空,不允許上傳

11、後端數據庫表設計

CREATE TABLE `file_upload_detail` (                                                                               
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',                                                           
  `username` varchar(64) NOT NULL COMMENT '上傳文件的用戶賬號',                                                            
  `file_name` varchar(64) NOT NULL COMMENT '上傳文件名',                                                               
  `md5` varchar(255) NOT NULL COMMENT '上傳文件的MD5值',                                                                
  `is_uploaded` int(11) DEFAULT '0' COMMENT '是否完整上傳過 \n0:否\n1:是',                                                 
  `has_been_uploaded` varchar(1024) DEFAULT NULL COMMENT '曾經上傳過的分片號',                                             
  `url` varchar(255) DEFAULT NULL COMMENT 'bos中的url,或者是本機的url地址',                                                 
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP  COMMENT '本條記錄創建時間',     
  `update_time` timestamp NULL DEFAULT NULL  COMMENT '本條記錄更新時間',                                                  
  `total_chunks` int(11) DEFAULT NULL COMMENT '文件的總分片數',                                                          
  PRIMARY KEY (`id`)                                                                                              
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8                                                             

12、關於什麼時候mergechunk

在本文中給出的demo中,merge是後端處理完成所有的chunk后,像前端返回 merge=1,這個表示來實現的。

前端拿着這個字段去發送/merge請求去合併所有的chunk。

值得注意的地方是:這個請求是在uploader.js認為所有的分片全部成功上傳后,在單個文件成功上傳的回調中執行的。我想了一下,感覺這麼搞其實不太友好,萬一merge的過程中失敗了,或者是某個chunk丟失了,chunk中的數據缺失,最終merge的產物的md5值其實並不等於原文件。當這種情況發生的時候,其實上傳是失敗的。但是後端既然告訴uploader.js 可以合併了,說明後端的upload函數認為任務是成功的。vue-simple-uploader上傳完最後一個chunk得到的狀態碼是200,它也會覺得任務是成功的,於是在前端段展示綠色的上傳成功給用戶看~(然而上傳是失敗的), 這麼看來,整個過程其實控制的不太好~

我現在的實現:直接幹掉merge請求,前端1條線程發送請求,將chunk依次發送到後端。後端檢測到所有的chunk都上傳過來後主動merge,merge完成后馬上校驗文件的md5值是否符合預期。這個處理過程在上傳最後一個chunk的請求中進行,因此可以實現的控制前端上傳成功還是失敗的樣式~
如果偏偏想追求極致的速度,可以考慮將後端更新isUpload字段的SQL換成 “select for update” 他可以鎖住你要更新的數據行
以及這一行上下的間隙,這樣就不會出現併發修改異常。前端也可以重新更換成多線程併發上傳的機制。理論上只要網絡帶寬允許你開啟五條線程,速度就快5倍。至於什麼時候merge,加個if判斷一下,當上傳過的分片數 == totalChunks 就可以merge了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧