文件MD5生成性能大提升!如何实现分片与Worker优化

在处理大文件上传时,生成文件的MD5哈希是一项重要的工作,可以用于文件校验、重复文件检查和数据完整性验证。然而,传统的MD5计算方法在处理大文件(如1GB以上的文件)时,往往效率较低。如何通过分片(chunking)技术和Web Worker实现MD5计算的显著性能提升。

我们通过这种优化方式,将1.67GB文件的MD5生成时间从18秒缩短到了仅3秒!

旧版实现:串行分片MD5计算

在原始的实现中,我们使用 FileReader 对文件进行分片处理,每次将分片转为ArrayBuffer,再通过SparkMD5库的增量计算功能逐片计算文件的MD5。虽然这种方法能够实现文件的逐片处理,但它仅使用单线程顺序计算,因此在大文件上会花费大量时间。以下是旧版实现的代码结构:

function createHash(chunks: Blob[]) {

return new Promise(resolve => {

const spark = new SparkMD5.ArrayBuffer()

function _hash(index: number) {

if (index >= chunks.length) {

resolve(spark.end())

return

}

const reader = new FileReader()

reader.readAsArrayBuffer(chunks[index])

reader.onload = e => {

spark.append(e.target?.result as ArrayBuffer)

_hash(index + 1)

}

}

_hash(0)

})

}

新版优化:利用Web Worker进行并行计算

新版实现的核心思路是通过Web Worker将分片处理和MD5计算过程并行化。具体来说,将文件划分为多个块,每个块通过Worker分发到独立的线程中进行处理,这样能够有效提高处理速度。

新版实现的关键步骤如下:

文件分块与分片:将文件划分为更大的块(例如50MB),并进一步拆分为小的分片(例如10MB),以适应Web Worker的处理。Worker管理与并发控制:通过线程池机制限制活跃Worker数量,避免性能瓶颈。并行计算进度与总哈希合并:每个Worker计算完一个块的MD5后,将结果传回主线程,主线程合并所有块的哈希,得出最终的MD5值。以下是新版优化中的关键代码:

const worker = new Worker('./hashWorker.js')

worker.postMessage({ chunks: chunkArray })

worker.onmessage = event => {

if (event.data.hash) {

workerResolve(event.data.hash)

} else if (event.data.progress) {

progress.value += (event.data.progress / totalSize) * 100

}

}

性能对比:3秒 vs 18秒

使用新版的优化方法,我们将1.67GB文件的MD5计算时间从18秒降低到了3秒,提升了6倍以上的速度。这种性能提升在处理大文件时尤为显著,为用户提供了更加流畅的文件上传体验。

整个流程如下

处理大文件的上传和哈希计算可能会面临性能瓶颈。以下是通过分块上传和 Web Worker 并发计算优化大文件 MD5 哈希值的完整步骤:

创建文件块和分片将大文件分割为若干个较大的块(如每块 50MB)。将每个块进一步拆分为更小的分片(如每片 10MB)。在 createChunks 函数中创建块结构,在 createChunksFromBlock 函数中完成从块到分片的转换。2.初始化 Worker

使用线程池管理 Web Worker 并发数量,限制同时处理的 Worker 数量,避免性能压力。通过 postMessage 方法向 Worker 发送分片和块的总字节数。3.在 Worker 中处理分片

在 Worker 中接收分片数据,利用 FileReader 逐片读取内容并通过 SparkMD5 进行增量哈希计算。每处理完一个分片后,向主线程发送进度(已处理字节数)和中间哈希值。4.计算总进度

主线程根据每个块的处理情况更新整体进度。每当一个块的哈希计算完成,增加已处理块计数,并相应更新总进度。5.合并哈希值

所有块的哈希计算完成后,主线程收集每个块的哈希值。使用 SparkMD5 合并各块的哈希,最终生成整个文件的 MD5 值。6.返回结果

所有块的处理完成后,返回最终文件的 MD5 哈希值和总进度(通常为 100%)。通过Web Worker与分片技术的结合,我们显著提升了大文件的MD5计算性能,这种方法不仅适用于MD5计算,也可用于其他大文件的处理场景。未来,我们可以进一步优化多线程逻辑,如增加断点续传、内存优化等,让文件处理更加高效!

完整代码

hashWorker.js// hashWorker.js

// importScripts('https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js')

importScripts('/node_modules/spark-md5/spark-md5.min.js')

self.onmessage = function (event) {

const chunks = event.data.chunks

const spark = new SparkMD5.ArrayBuffer()

let loaded = 0

const readChunk = index => {

if (index >= chunks.length) {

self.postMessage({ hash: spark.end() })

return

}

const fileReader = new FileReader()

fileReader.onload = e => {

spark.append(e.target.result)

loaded++

const chunkSize = chunks[index].size

// 发送当前块已读字节数

// 当前已读字节数

const progress = loaded * chunkSize

// 发送当前块的进度

self.postMessage({ progress, size: chunkSize })

readChunk(index + 1)

}

fileReader.onerror = () => {

self.postMessage({ error: 'File reading error' })

}

fileReader.readAsArrayBuffer(chunks[index])

}

readChunk(0)

}

2.useFileChunks

// useFileChunks.ts

import { ref } from 'vue'

import SparkMD5 from 'spark-md5'

/**

*

* 大文件分块上传和哈希

* 1. 创建文件块和分片:

* 1.1 将文件分成若干个较大的块(例如 50MB),每个块内部再分成更小的分片(例如 10MB)。

* 1.2 在 createChunks 函数中实现块的创建,并在 createChunksFromBlock 函数中实现从块到分片的转换。

*

* 2. 初始化 Worker:

* 2.1 在处理每个块时,使用线程池管理并发的 Web Worker,限制同时处理的 Worker 数量以避免性能问题。

* 2.2 通过 postMessage 方法将分片和当前块的总字节数发送给 Worker。

*

* 3. 在 Worker 中处理分片:

* 3.1 在 Worker 中接收分片数据,通过 FileReader 读取每个分片的内容,并使用 SparkMD5 计算每个分片的哈希值。

* 3.2 每读取完一个分片,向主线程发送当前进度(已读取字节数)和最终的哈希值。

*

* 4. 计算总进度:

* 4.1 在主线程中,根据每个块的处理进度,计算和更新总进度。

* 4.2 每当一个块的哈希计算完成时,增加已处理的块计数,并根据已处理块数更新总进度。

*

* 5. 合并哈希值:

* 在所有块的哈希计算完成后,收集每个块的哈希值,并使用 SparkMD5 合并这些哈希值,得到最终的文件哈希。

*

* 6. 返回结果:

* 在所有处理完成后,返回文件的 MD5 哈希值和当前进度(可能达到 100%)。

* @param blockSize 块大小 (默认 50MB)

* @param chunkSize 分片大小 (默认 10MB)

* @returns {md5: string, progress: number}

*/

export const useFileChunk = (

blockSize = 50 * 1024 * 1024,

chunkSize = 10 * 1024 * 1024,

) => {

const md5 = ref()

const progress = ref(0)

const activeWorkers = ref(0)

// 设置最大 Worker 数量

const MAX_WORKERS = 4

// 创建块和分片

function createChunks(file: File) {

const blocks = []

let cur = 0

// 分块

while (cur < file.size) {

const block = file.slice(cur, cur + blockSize)

blocks.push(block) // 保存块本身以计算字节数

cur += blockSize

}

return blocks

}

// 从块中创建分片

function createChunksFromBlock(block: Blob) {

const result = []

let cur = 0

while (cur < block.size) {

const chunk = block.slice(cur, cur + chunkSize)

result.push(chunk)

cur += chunkSize

}

return result

}

async function get(file: File) {

if (!file) throw new Error('File is required')

progress.value = 0

md5.value = ''

const blocks = createChunks(file) // 创建块和分片

const totalBlocks = blocks.length // 计算块总数

const workerPromises = blocks.map(block => {

return new Promise((workerResolve, workerReject) => {

const processBlock = () => {

if (activeWorkers.value < MAX_WORKERS) {

activeWorkers.value++

const worker = new Worker(

new URL('./hashWorker.js', import.meta.url),

)

const chunkArray = createChunksFromBlock(block)

// 将块中的所有片发送给 Worker

worker.postMessage({ chunks: chunkArray })

worker.onmessage = event => {

if (event.data.hash) {

workerResolve(event.data.hash) // 返回当前块的哈希

activeWorkers.value-- // 完成后减少活跃 Worker 数量

} else if (event.data.size !== undefined) {

// 更新块进度

progress.value += (event.data.size / file.size) * 100

} else if (event.data.error) {

workerReject(event.data.error)

activeWorkers.value-- // 出错时也减少活跃 Worker 数量

}

}

worker.onerror = error => {

workerReject('Worker error: ' + error.message)

activeWorkers.value-- // 出错时减少活跃 Worker 数量

}

} else {

// 如果当前活跃 Worker 已满,稍后重试

setTimeout(processBlock, 100)

}

}

processBlock() // 启动块处理

})

})

// 等待所有块的哈希返回

const hashes: string[] = (await Promise.all(

workerPromises,

)) as unknown as string[]

// 合并哈希值

const finalSpark = new SparkMD5()

hashes.forEach(hash => finalSpark.append(hash))

md5.value = finalSpark.end() // 计算最终的 MD5 值

return { md5: md5.value, progress: progress.value, chunks: [] }

}

return { getFileInfo: get, progress }

十二星座黑化排行榜!谁最令人胆战心惊?
AI驱动阿里云估值重建