最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于SpringBoot和Vue實現(xiàn)分片上傳系統(tǒng)

 更新時間:2023年12月08日 11:07:13   作者:懂咖啡的Java實習生  
最近想做一個關于文件上傳的個人小網盤,一開始嘗試使用了OSS的方案,但是該方案對于大文件來說并不友好,所以開始嘗試分片上傳方案的探索,接下來小編給大家詳細的介紹一下如何基于SpringBoot和Vue實現(xiàn)分片上傳系統(tǒng),需要的朋友可以參考下

最近想做一個關于文件上傳的個人小網盤,一開始嘗試使用了OSS的方案,但是該方案對于大文件來說并不友好,一個是OSS云服務廠商費用高昂的問題,另外一個是大文件速度較慢。于是看了網絡上的帖子以及工作室小伙伴的推薦,開始嘗試分片上傳方案的探索,目前整個項目已經完成,本人認為使用的技術都是最簡單且高效的方法,主要采用自己編寫的方案,在應用層比較少使用到第三方的技術,主要用到的技術有Vue+SpringBoot+MySQL+Redis。此次展示分享上傳部分,感興趣的小伙伴們可以點贊評論,我會在后面及時更新!

首先第一步是整個上傳過程中最重要的一環(huán),對文件內容而并非標題進行一個md5編碼,基于每一個文件一個唯一的字符串,后續(xù)所有文件相關的處理都需要使用到該字符串。這里的計算過程中,采取了黑馬在知乎上一篇文章的建議,對文件第一個分片和最后一個分片進行全部計算,其他地方采用前中后兩個字節(jié)進行計算,這樣子可以減少計算量,加快我們的編碼速度,此步驟據說也有開源的框架可以代替,這樣子可靠性也更高,有興趣的小伙伴可以自己了解,下面附上自己實現(xiàn)的。

    async calculateHash(fileChunks) {
      return new Promise(resolve => {
        const spark = new sparkMD5.ArrayBuffer()
        const chunks = []
        const CHUNK_SIZE = this.CHUNK_SIZE
        fileChunks.forEach((chunk, index) => {
          if (index === 0 || index === fileChunks.length - 1) {
            // 1. 第一個和最后一個切片的內容全部參與計算
            chunks.push(chunk.file)
          } else {
            // 2. 中間剩余的切片我們分別在前面、后面和中間取2個字節(jié)參與計算
            // 前面的2字節(jié)
            chunks.push(chunk.file.slice(0, 2))
            // 中間的2字節(jié)
            chunks.push(chunk.file.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2))
            // 后面的2字節(jié)
            chunks.push(chunk.file.slice(CHUNK_SIZE - 2, CHUNK_SIZE))
          }
        })
        const reader = new FileReader()
        reader.readAsArrayBuffer(new Blob(chunks))
        reader.onload = (e) => {
          spark.append(e.target.result)
          resolve(spark.end())
        }
      })
    }

在計算完畢之后,我們可以在上傳之前可以先做一次檢查,返回我們需要得到的信息,包括但不限于文件是否存在于系統(tǒng)中,文件沒有上傳的話,那么是否有已經上傳了的分片,可以返回一個索引數(shù)組。如果已經有該文件存在的話則直接進行保存文件信息就好了,后者有利于實現(xiàn)我們的斷點續(xù)傳工作,第二次上傳只要上傳還沒有上傳的部分即可,主要是后端實現(xiàn)為主。

    async uploadCheck() {
      let r;
      await axios.get('/api/file//uploadCheck?fileMd5=' + this.key).then(res => {
        r = res.data.flag;
        this.existCheck=[];
        //存在部分分片則返回存在的文件信息
        if (r == false){
          this.existCheck=res.data.data;
        }
      })
      return r;
    }

后端代碼分為接口和服務層代碼,分別給出:

    /**
     * 文件整體的查重校驗
     * @param fileMd5
     * @return
     */
    @GetMapping("/uploadCheck")
    public Result uploadCheck(String fileMd5,HttpServletRequest httpServletRequest){
        String user = JwtUtil.getId(httpServletRequest.getHeader("token"));
        if (fileService.uploadCheck(fileMd5,user)){
            return new Result(true,true);
        }else {
            //查找文件是否有分片上傳過到系統(tǒng)中
            Integer arr[] = fileService.existCheck(fileMd5);
            return new Result(false,arr);
        }
    }

上傳時候如果在數(shù)據庫中發(fā)現(xiàn),已經有用戶或者本用戶在系統(tǒng)中已經成功上傳過該文件的話,那么我們可以直接插入數(shù)據返回保存完畢即可了,無需真正意義上的上傳。不存在則在redis中看一下那些索引已經上傳過了,將索引數(shù)組返回,前端后續(xù)上傳跳過即可。

    @Override
    public Boolean uploadCheck(String fileMd5, String userId) {
        //判斷文件是否存在
        MyFile myFile = fileMapper.getFileByMd5(fileMd5);
        //如果文件存在直接給用戶插入數(shù)據記錄即可
        if (myFile != null) {
            MyFile newMyFile = new MyFile();
            newMyFile.setId(userId + DateTimeUtil.getTimeStamp());
            newMyFile.setFileName(myFile.getFileName());
            newMyFile.setUser(userId);
            newMyFile.setFileMd5(fileMd5);
            newMyFile.setFileSize(myFile.getFileSize());
            newMyFile.setTime(LocalDateTime.parse(DateTimeUtil.getDateTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            Integer num = fileMapper.insert(newMyFile);
            if (num == 1) {
                return true;
            }
        }
        return false;
    }

檢查完畢之后我們可以開始上傳文件啦!上傳過程中我認為依然是前端占了打頭的,后端只要接收文件不斷磁盤寫入就好了,雖然說不建議那么多的io次數(shù),但是實際上測試下來還可以,4M帶寬的學生服務器都可以做到20秒左右上傳100M,本地的話更加是快的不得了,反而前端如果分片分的太小的話,觸發(fā)的網絡請求數(shù)量過多,這時候速度上才容易出事,前端分片不要設置太小的話,io次數(shù)的話也可以小一點。

      const formDatas = data.map(({chunk, fileHash, index, filename, chunkSize}) => {
        const formData = new FormData()
        // 切片文件
        formData.append('file', chunk)
        // 大文件hash
        formData.append('fileMd5', fileHash)
        //切片的索引
        formData.append('currentIndex', index)
        // 大文件的文件名
        formData.append('fileName', filename)
        // 分片大小
        formData.append('chunkCount', chunkSize)
        return formData
      })
      let index = 0;
      const max = 6; // 并發(fā)請求數(shù)量
      const taskPool = []// 請求隊列
      let t = this.existCheck.length;
      while (index < formDatas.length) {
        //出現(xiàn)重復的切片,跳過
        if (this.existCheck.includes(index)){
          index++;
          continue;
        }
        const task = axios.post('/api/file/uploadBySlice', formDatas[index])
        //splice方法會刪除數(shù)組中第一個匹配的元素,參數(shù)搭配使用findIndex可以找到第一個匹配的元素的索引
        task.then(() => {
          taskPool.splice(taskPool.findIndex((item) => item === task))
          t=t+1;
          this.percentage = Math.floor((t / formDatas.length * 100) * (1.0))-1
        })
        taskPool.push(task);
        if (taskPool.length === max) {
          // 當請求隊列中的請求數(shù)達到最大并行請求數(shù)的時候,得等之前的請求完成再循環(huán)下一個
          await Promise.race(taskPool)
        }
        index++
      }
      await Promise.all(taskPool)
    }

后端代碼實現(xiàn),接口層簡單,只展示服務層即可。后端主要負責的工作,包括分片寫入磁盤,并且在redis中保存已經上傳好的文件索引號。

    /**
     * 上傳分片、文件
     *
     * @param file
     * @param fileMd5
     * @param currentIndex
     * @return
     */
    @Override
    public Integer uploadFile(MultipartFile file, String fileMd5, Integer currentIndex) {
        //在redis中查詢該分片是否已經存在
        if (redisTemplate.opsForSet().isMember(fileMd5, currentIndex)) {
            return currentIndex;
        }
        // 生成分片的臨時路徑
        String filePath = tempPath + fileMd5 + "_" + currentIndex + ".tmp";
        //保存文件分片到本地的目標路徑
        File targetFile = new File(filePath);
        try {
            RandomAccessFile raf = new RandomAccessFile(targetFile, "rw");
            byte[] data = file.getBytes();
            raf.write(data);
            raf.close();
            //在redis中保存該分片的索引
            redisTemplate.opsForSet().add(fileMd5, currentIndex);
            return currentIndex;
        } catch (IOException e) {
            // 處理異常
            throw new ServiceException("文件上傳失敗");
        }
    }

那么,如果我們我們上傳一次中間不小心刷新或者網絡中斷后,我們應該如何處理呢?其實在前面的時候我們已經解決了,因為我們上傳檢查的時候,已經返回了已經上傳過的索引號,所以這一次上傳的時候自動跳過即可了,在上面前端的上傳區(qū)域可以看見有跳過的代碼設置!

最后文件分片都上傳完畢了,我們就是最后一步了,等待前端所有的上傳任務執(zhí)行完畢,我們執(zhí)行一次發(fā)送合并指令即可了,當然在后端其實也可以做,前端就不需要發(fā)送合并指令了。

    //發(fā)起文件合并請求
    merge() {
      axios.get('/api/file/merge' + '?fileMd5=' + this.key + '&fileName=' + this.filename + '&chunkCount=' + this.fileChunks.length).then((resp) => {
        if (resp.data.flag == true) {
          this.$message({
            message: '文件上傳成功',
            type: 'success'
          });
          this.percentage = 100;
          this.query();
          this.uploadRefresh=false;
        }
      })
    }

后端此處代碼比較長,但是實際上也比較簡單的,主要是做了合并故障的處理,和剛才前端上傳故障處理的思路類似,如果出現(xiàn)故障,下一次合并從斷點繼續(xù)就好了,這一次的邏輯從前端搬到了后端來做,也是通過redis來記錄。

    /**
     * 合并分片文件
     *
     * @param fileName
     * @param chunkCount
     * @return
     */
    @Override
    public String mergeTmpFiles(String fileMd5, String fileName, Integer chunkCount, String userId) throws IOException {
        //記錄本次合并的字節(jié)數(shù)
        long count = 0;
        //獲取分片索引號的起始地址
        int start = 0 ;
        String countKey = fileMd5+"-count";
        if (!redisTemplate.hasKey(countKey)) {
            redisTemplate.opsForHash().put(countKey, "count", "0");
        }else {
            start = Integer.parseInt(redisTemplate.opsForHash().get(countKey, "count").toString());
            start++;
        }
        //記錄分片文件的總大小
        for (int i = start; i < chunkCount; i++) {
            //讀取分片文件
            String filePath = tempPath + fileMd5 + "_" + i + ".tmp";
            File file = new File(filePath);
            if (!file.exists()) {
                //需要排除redis造成的異常情況
                redisTemplate.opsForSet().remove(fileMd5, i);
                log.info("缺失索引編號", i);
                throw new ServiceException("文件分片缺失");
            } else {
                count += file.length();
            }
            //使用緩沖流讀取到內存中
            byte[] data = new byte[(int) file.length()];
            FileInputStream inputStream = new FileInputStream(file);
            inputStream.read(data);
            inputStream.close();
            //保存文件到文件夾中
            file = new File(endPath + fileMd5 + "." + getFileExtension(fileName));
            FileOutputStream outputStream = new FileOutputStream(file, true);
            outputStream.write(data);
            outputStream.close();
            //刪除碎片文件
            File temp = new File(filePath);
            temp.delete();
            //記錄合并進度
            redisTemplate.opsForHash().put(countKey, "count", i);
        }
        //記錄文件保存數(shù)據
        MyFile myFile = new MyFile();
        myFile.setId(userId + DateTimeUtil.getTimeStamp());
        myFile.setFileName(fileName);
        myFile.setUser(userId);
        myFile.setFileMd5(fileMd5);
        File file = new File(endPath + fileMd5 + "." + getFileExtension(fileName));
        myFile.setFileSize(file.length());
        myFile.setTime(LocalDateTime.parse(DateTimeUtil.getDateTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        fileMapper.insert(myFile);
        //刪除各類緩存數(shù)據
        redisTemplate.delete(countKey);
        redisTemplate.delete(fileMd5);
        //返回處理結果
        return fileName + "  此次合并:" + count + "字節(jié)";
    }

好啦!分片上傳,斷點上傳,秒傳等功能已經全部實現(xiàn)啦!合并文件或者故障處理等都已經自己在后端打斷點測試過,可靠性較高。在本地跑用的是8核+24G配置,沒有出過什么故障,但是上傳到本人2核+2G的機器上,超過100M的文件,偶爾合并會出現(xiàn)故障,但是通過合并的故障處理,我們可以讓前端如果合并失敗的話,再次發(fā)起合并請求即可,目前還沒有出現(xiàn)過連續(xù)合并請求兩次都不成功的,而且第二次合并請求也是在斷點的基礎上進行的,沒有白費消耗。

以上就是基于SpringBoot和Vue實現(xiàn)的分片上傳系統(tǒng)的詳細內容,更多關于SpringBoot Vue分片上傳系統(tǒng)的資料請關注腳本之家其它相關文章!

相關文章

  • Java將網絡圖片轉成輸入流以及將url轉成InputStream問題

    Java將網絡圖片轉成輸入流以及將url轉成InputStream問題

    這篇文章主要介紹了Java將網絡圖片轉成輸入流以及將url轉成InputStream問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • Java垃圾收集之對象存活判定、回收流程與內存策略詳解

    Java垃圾收集之對象存活判定、回收流程與內存策略詳解

    Java對象的內存分配主要發(fā)生在堆內存(新生代、老年代),少數(shù)情況可能分配在棧(棧上分配)或直接內存(堆外內存),這篇文章主要介紹了Java垃圾收集之對象存活判定、回收流程與內存策略的相關資料,需要的朋友可以參考下
    2026-03-03
  • 在springboot中使用AOP進行全局日志記錄

    在springboot中使用AOP進行全局日志記錄

    這篇文章主要介紹就在springboot中使用AOP進行全局日志記錄,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • 解決spring AOP中自身方法調用無法應用代理的問題

    解決spring AOP中自身方法調用無法應用代理的問題

    這篇文章主要介紹了解決spring AOP中自身方法調用無法應用代理的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • StringRedisTemplate操作hash實現(xiàn)過程

    StringRedisTemplate操作hash實現(xiàn)過程

    文章總結了使用StringRedisTemplate操作Redis哈希(hash)的基本示例,分享了個人經驗,旨在為讀者提供參考,并鼓勵大家支持腳本之家
    2026-03-03
  • MyBatis通用Mapper實現(xiàn)原理及相關內容

    MyBatis通用Mapper實現(xiàn)原理及相關內容

    今天小編就為大家分享一篇關于MyBatis通用Mapper實現(xiàn)原理及相關內容,小編覺得內容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2018-12-12
  • java圖片格式轉換的三段代碼

    java圖片格式轉換的三段代碼

    這篇文章主要分享了java圖片格式轉換的三段代碼,小編查找資料整理了java圖片格式轉換的三段不同代碼,推薦給大家。
    2015-11-11
  • Java如何使用Set接口存儲沒有重復元素的數(shù)組

    Java如何使用Set接口存儲沒有重復元素的數(shù)組

    Set是一個繼承于Collection的接口,即Set也是集合中的一種。Set是沒有重復元素的集合,本篇我們就用它存儲一個沒有重復元素的數(shù)組
    2022-04-04
  • springboot使用redis的詳細步驟

    springboot使用redis的詳細步驟

    SpringBoot對常用的數(shù)據庫支持外,對NoSQL?數(shù)據庫也進行了封裝自動化,下面這篇文章主要給大家介紹了關于springboot使用redis的詳細步驟,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-06-06
  • Intellij IDEA 2020.3 配置教程詳解

    Intellij IDEA 2020.3 配置教程詳解

    這篇文章主要介紹了Intellij IDEA 2020.3 配置教程詳解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-02-02

最新評論

永嘉县| 绥棱县| 卓资县| 安化县| 喀喇| 增城市| 黑河市| 祁东县| 永德县| 万荣县| 天长市| 武城县| 雷波县| 合水县| 澎湖县| 商都县| 南华县| 山丹县| 靖远县| 沁阳市| 靖江市| 龙胜| 呼图壁县| 镇巴县| 神木县| 油尖旺区| 济宁市| 肥西县| 汤原县| 新沂市| 普洱| 曲水县| 武城县| 永善县| 临武县| 永寿县| 黑山县| 称多县| 和顺县| 昌都县| 凤翔县|