前言
刚刚要吃饭,放下书包 , 适才掏出手机点开支付码 , 微信嘀嘟一声 , 划过一条消息 :过佬 发来一张图片 , 我好奇地点开群聊消息 , 点入图片 , 不禁一惊 , 过佬出征字节了😱 , 面经如下 :
现在都流行问场景题呢👈 , 之前在掘金上看到个观点 : 在 AI 大行其道 の 今天 , 八股一问便有答案 , 也是说在一定程度上打破了信息差 ; 未来在传统八股方面会减弱 , 将会聚焦与场景化 , 各位掘友怎被看 ?
哈哈 , 不管怎么样😏 , 如果八股背成了八股 , 自然无用 , 利用八股解决实际问题 ,有何尝不是场景化地具体实现呢 ?
今天 , 开始一起研究下大文件上传吧 ~
我之前写过一篇文章关于大文件上传 , 主要是用于实践 , 很多细节没有挖掘哈~
大文件上传👈 | React + NestJs |分片、断点续传、秒传🚀 , 你是否知道 ???
现在进行深入拷打 ~
就是因为这篇文章 , 我也被拷打过😭 , 面经如下 :
完整的面试经过可以查看我之前写的一篇面经:
2025年2月凉面与热面(1)------杭州AI公司一二面
过总
先看过佬的面经
问题一 : 大文件上传以及应用场景优化
这个是十分大的问题 , 拆解下来就是应用场景 和性能优化 , 可以先泛泛谈之 ~
应用场景
这边举一些例子:
- 云存储:用户向百度网盘、阿里云盘等上传大型视频、软件安装包等,利用大文件上传功能,即便网络不稳定也能成功上传。
- 企业办公:设计师上传大型设计素材文件到公司共享服务器,或程序员上传项目部署包等。
- 在线视频制作:创作者将本地高清视频素材上传到在线剪辑平台。
在这些例子里面 , 我有个点有深刻影响 🤡👈 ------ 程序员上传项目部署包 , 我之前文章提到 (本地构建/手动上传/服务器运行) 的部署方案 , 在这个过程中 , 我就需要在本地打包上传服务器 , 这就是大文件上传 ~
我的远程实习(四)| Ailln叫我docker部署项目,我顺便填了以前的坑
性能优化
- 网络层面:利用 CDN(内容分发网络),将文件切片缓存到离用户近的节点,加快传输速度。如腾讯云 CDN 可加速文件上传(旨在通过将网站内容缓存到全球各地的节点,实现快速、稳定和高效的访问体验)
- 前端优化:使用多线程或 Web Workers 实现多切片并发上传。像 JavaScript 借助 Web Workers 可开启多个线程同时上传不同切片。
- 后端优化:优化服务器存储和文件合并逻辑,采用分布式存储系统,如 Ceph,提升存储和处理能力。
问题二:断点续传,怎么确认上传完了
断点续传原理 :在上传过程中记录已上传切片信息,网络中断恢复后,从上次中断处继续上传。例如,上传 100 个切片的文件,传到第 30 个时断网,恢复后从第 31 个开始。
确认上传完成方式
- 计数确认:前端记录已上传切片数量,后端接收切片时也记录。当两端记录的已上传切片数都等于总分片数,确认上传完成。比如前端记录上传了 100 个切片中的 100 个,后端也接收了 100 个,即上传完毕。
- 哈希校验:对完整文件计算哈希值(如 MD5、SHA - 1 等),上传过程中服务器对合并后的文件计算哈希值,两者相等则确认上传完成。例如上传一个软件安装包,本地计算其 MD5 值,服务器合并后计算 MD5 值与之比对。
问题三:用户上传到一半,重新刷新页面,要不要重新上传
不需要重新上传的情况 :若前端和后端有完善的断点续传机制,且能记录已上传切片信息。比如前端使用 IndexedDB 存储已上传切片索引,后端数据库记录接收情况。刷新页面后,前端从** IndexedDB** 读取信息,和后端核对,接着传未上传切片。
需要重新上传的情况:如果没有有效的状态记录和断点续传机制,刷新页面后前端无法知晓已上传进度,可能会重新上传。例如简单的单线程上传脚本,没做任何状态保存,刷新后只能从头开始。
问题四:文件分片的 id 记录在哪
前端记录
- 内存变量 :在 JavaScript 中,可定义变量存储切片 id。如
let uploadedChunkIds = [];
,每上传一个切片,将其 id push 进数组。适用于简单页面,页面刷新数据丢失。 - 本地存储 :使用
localStorage
或IndexedDB
。如localStorage.setItem('chunkIds', JSON.stringify(uploadedChunkIds));
,可长期保存,支持断点续传,适用于复杂大文件上传场景。
后端记录
- 数据库:存入关系型数据库(如 MySQL)或非关系型数据库(如 MongoDB) 。以 MySQL 为例,建表记录文件 id、切片 id、上传状态等信息。方便服务器端管理和查询,支持多用户、大规模文件上传场景。
- 缓存:如 Redis,可快速记录和查询切片 id 状态。适合高并发场景,能提升查询和处理速度。
以上就是过佬面经中的大文件上传了,接下来再来拷打一下 !!! 分别从不同角度拷打 , 可能答案相似之处甚多
我的
以下是对图中文件上传相关问题的详细深刻解释:
如何并发的执行文件上传
- 原理 :利用浏览器或运行环境的多线程、异步特性,同时发起多个文件切片的上传请求。比如在前端 JavaScript 中,可将每个切片的上传任务封装成 Promise,再借助
Promise.all
等方式并发执行。 - 实现方式 :先把大文件切片,如将一个 500MB 的文件切成 500 个 1MB 的切片。然后为每个切片创建上传任务,像使用
XMLHttpRequest
或Fetch API
来发起请求。例如用Fetch API
时,代码类似fetch('/upload', { method: 'POST', body: chunk })
(chunk
为切片数据),最后通过Promise.all
并发执行这些请求。 - 注意事项:需控制并发数量,避免因过多并发请求耗尽网络资源或导致浏览器性能下降。可通过自定义队列等方式,设定最大并发数,如设置为 5,当正在执行的上传任务小于 5 时,才从任务队列中取出新切片进行上传。
一般的并发数是多少
- 影响因素 :
- 网络环境:在高速稳定的企业专线网络下,可适当提高并发数;而在普通家庭宽带或移动网络环境中,过高并发数可能导致网络拥塞,一般并发数不宜过高。
- 服务器性能:服务器配置高、带宽充足,能承受较多并发请求;若服务器性能有限,过多并发会使其负载过高,影响服务稳定性。
- 浏览器限制:不同浏览器对并发连接数有不同限制,比如 Chrome 浏览器对同一个域名的并发连接数一般限制在 6 - 8 个左右 。
- 常见取值范围:通常在 3 - 10 之间。对于小型项目或对实时性要求不高的场景,3 - 5 较为合适;而在一些大型文件存储系统且网络和服务器条件较好时,可能会设置到 8 - 10 。但实际应用中需通过测试来确定最优并发数。
如果分片5片,其中两片传完了,接下来怎么办
- 正常情况:继续按顺序或并发上传剩余的 3 片。若采用顺序上传,依次发起对剩余切片的上传请求;若采用并发上传且还有并发名额,将剩余切片的上传任务加入并发队列执行。
- 异常处理:若在上传过程中发现已上传的两片存在问题(如通过 MD5 校验发现文件损坏 ),需重新上传这两片。同时,若上传过程中网络中断,需记录已上传切片的状态,待网络恢复后,根据记录继续上传剩余切片或重新上传有问题的切片。
- 后端协作:后端需配合记录已成功接收的切片信息,以便前端在各种情况下能准确判断上传进度和下一步操作。比如后端可在数据库中记录每个切片的接收状态 。
文件切片大一点好,还是小一点好? 分片切多少怎么考虑
- 切片大的优劣
- 优点:切片数量少,上传任务调度开销小,合并操作相对简单,在网络稳定且带宽充足时,能快速完成上传。例如在企业内部高速网络环境下上传大型安装包。
- 缺点:单个切片传输时间长,网络不稳定时,易因超时等问题导致整个切片重传,浪费时间和流量。
- 切片小的优劣
- 优点:单个切片传输快,网络波动影响小,便于实现断点续传,重传成本低。比如在网络状况复杂的公共 Wi - Fi 环境中上传文件。
- 缺点:切片数量多,调度和管理开销大,后端合并操作也更复杂。
- 考虑因素
- 网络状况:网络稳定且带宽大,可适当增大切片;网络不稳定则宜采用较小切片。
- 文件类型:对实时性要求高的文件(如视频流 ),小切片可减少卡顿;对完整性要求高的文件(如可执行程序 ),大切片可能更合适。
- 服务器性能:服务器处理能力强、存储 I/O 性能高,可处理较多切片;若服务器性能有限,大切片可减轻其处理压力。
如何实现秒传?(MD5值比对)
- 原理:利用文件内容的唯一性,通过计算文件的 MD5 值(一种哈希算法,能为文件生成唯一的固定长度字符串 )来标识文件。用户上传文件前,先计算其 MD5 值并发送给服务器,服务器在存储中查找是否有相同 MD5 值的文件。若有,直接将该文件与用户关联,实现秒传;若无,则开始正常上传流程。
- 实现步骤
- 前端计算:在前端使用相关库(如 js - md5 库 )计算文件的 MD5 值。
- 服务器查找:服务器接收到 MD5 值后,在文件索引数据库中查询是否存在相同 MD5 值的文件记录。
- 结果处理:若找到,向用户返回已存在文件的信息,完成秒传;若未找到,通知前端开始上传文件切片,并在上传完成后将新文件及其 MD5 值记录到数据库。
- 安全性和局限性:MD5 算法存在碰撞概率(不同文件可能有相同 MD5 值 ),但概率极低。在实际应用中,可结合其他校验方式(如文件大小、文件头信息等 )提高准确性。
前端如何实现并发上传的 ?
大文件并发上传通常按以下步骤和方式实现:
1. 文件切片
先把大文件分割成若干较小的数据块,比如将一个 100MB 的文件按 1MB 大小切成 100 个切片。在前端,利用 JavaScript 的 Blob.prototype.slice
方法就能实现,示例代码如下:
javascript
function splitFile(file, chunkSize = 5 * 1024 * 1024) {
const chunks = [];
let start = 0;
while (start < file.size) {
const chunk = file.slice(start, start + chunkSize);
chunks.push(chunk);
start += chunkSize;
}
return chunks;
}
2. 并发控制策略
- 基于
Promise.all
并发 :将每个切片的上传请求封装成Promise
对象,存进数组,再用Promise.all
并发执行这些Promise
。举例:
javascript
const chunks = splitFile(file);
const promises = chunks.map((chunk, index) => {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
return fetch('/upload', { method: 'POST', body: formData });
});
await Promise.all(promises);
不过,这种方式可能因同时发起过多请求,耗尽系统资源或造成网络拥塞。
- 队列控制并发数:设定最大并发数,用队列管理切片上传。比如设置最大并发数为 5,上传队列里存放待上传切片,当前上传数小于 5 且队列有切片时,就取出切片上传。示例代码:
javascript
const MAX_CONCURRENT_UPLOADS = 5;
const uploadQueue = [];
let activeUploads = 0;
function enqueueUpload(file) {
uploadQueue.push(file);
processQueue();
}
function processQueue() {
if (activeUploads < MAX_CONCURRENT_UPLOADS && uploadQueue.length > 0) {
const file = uploadQueue.shift();
activeUploads++;
uploadFile(file).finally(() => {
activeUploads--;
processQueue();
});
}
}
3. 前端上传实现
- 利用 XMLHttpRequest 或 Fetch API :通过这两个 API 发起切片上传请求。以
XMLHttpRequest
为例:
javascript
function uploadChunk(chunk, index) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
xhr.onload = () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error('Chunk upload failed'));
}
};
xhr.onerror = () => {
reject(new Error('Upload error'));
};
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
xhr.send(formData);
});
}
4. 后端处理
后端接收切片请求,可借助像 Express.js(Node.js)、Django(Python)等框架。以 Express.js 为例,示例代码如下:
javascript
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('chunk'), (req, res) => {
// 处理接收到的切片,可记录切片信息到数据库等
res.status(200).send('Chunk received successfully');
});
5. 切片合并
所有切片都上传到后端后,要按顺序合并成原始文件。比如在 Node.js 里,使用 fs.appendFileSync
方法,先创建空文件,再把切片数据依次追加进去:
javascript
const fs = require('fs');
const path = require('path');
function mergeChunks(chunkPaths, outputPath) {
const writeStream = fs.createWriteStream(outputPath);
chunkPaths.forEach((chunkPath) => {
const readStream = fs.createReadStream(chunkPath);
readStream.pipe(writeStream, { end: false });
readStream.on('end', () => {
fs.unlinkSync(chunkPath); // 合并后删除临时切片文件
});
});
writeStream.on('finish', () => {
console.log('File merged successfully');
});
}
此外,部分云存储服务(如阿里云 OSS 等)的 SDK 也提供了并发上传功能,像 Java SDK 用 taskNum
、Python SDK 用 num_threads
参数来控制并发数 ,使用时按对应 SDK 文档配置即可实现大文件并发上传。
除了采用 promise.all 并发上传 , 你还知道什么 ?
除了基于Promise.all
并发外,还有以下并发控制策略:
自定义队列控制
- 原理:维护一个任务队列和一个记录当前正在执行任务数量的变量。设定最大并发数,当有新任务时,先放入队列。若当前执行任务数小于最大并发数,从队列取出任务执行;任务完成后,减少当前执行任务数,并检查队列,若有剩余任务则继续取出执行。
- 示例代码(JavaScript):
javascript
class TaskQueue {
constructor(maxConcurrent) {
this.maxConcurrent = maxConcurrent;
this.currentCount = 0;
this.queue = [];
}
addTask(taskFn) {
return new Promise((resolve, reject) => {
this.queue.push({ taskFn, resolve, reject });
this.processQueue();
});
}
processQueue() {
if (this.currentCount >= this.maxConcurrent || this.queue.length === 0) {
return;
}
const { taskFn, resolve, reject } = this.queue.shift();
this.currentCount++;
taskFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.currentCount--;
this.processQueue();
});
}
}
// 使用示例
const taskQueue = new TaskQueue(3);
const tasks = Array.from({ length: 10 }, (_, i) => () => new Promise((resolve) => setTimeout(() => {
console.log(`Task ${i} completed`);
resolve();
}, 1000 * (i + 1))));
const executeTasks = async () => {
const promises = tasks.map(task => taskQueue.addTask(task));
await Promise.all(promises);
console.log('All tasks completed');
};
executeTasks();
生成器函数结合yield
- 原理 :利用生成器函数可以暂停和恢复执行的特性,配合
yield
关键字手动控制异步任务的执行顺序和并发情况。在生成器函数内部逐个生成异步任务,每次yield
一个任务,等待其完成后再继续执行下一个任务。 - 示例代码(JavaScript):
javascript
function* taskGenerator() {
yield new Promise((resolve) => setTimeout(() => {
console.log('Task 1 completed');
resolve();
}, 1000));
yield new Promise((resolve) => setTimeout(() => {
console.log('Task 2 completed');
resolve();
}, 1500));
yield new Promise((resolve) => setTimeout(() => {
console.log('Task 3 completed');
resolve();
}, 2000));
}
const runner = async function () {
const gen = taskGenerator();
let result;
do {
result = gen.next();
if (!result.done) {
await result.value;
}
} while (!result.done);
console.log('All tasks in generator completed');
};
runner();
消息队列
- 原理:将异步任务放入消息队列中,由多个消费者(可以是线程、进程等)从队列中获取任务并并行处理。消息队列会按照一定规则(如先进先出)分配任务给消费者,能处理大量异步任务且允许一定延迟。适用于后端系统处理高并发任务场景,像大型电商系统处理订单、物流等任务。
- 示例(以RabbitMQ为例,Python语言):
python
import pika
# 连接RabbitMQ服务器
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# 声明队列
channel.queue_declare(queue='task_queue')
# 生产者发送任务消息
for i in range(10):
message = f"Task {i}"
channel.basic_publish(exchange='', routing_key='task_queue', body=message)
print("Tasks sent to queue")
# 关闭连接
connection.close()
# 消费者接收并处理任务(另一个Python脚本示例)
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='task_queue')
def callback(ch, method, properties, body):
print(f"Received and processed: {body}")
channel.basic_consume(queue='task_queue', on_message_callback=callback, auto_ack=True)
print('Waiting for tasks...')
channel.start_consuming()
限流算法
- 令牌桶算法:系统按固定速率生成令牌放入桶中,桶有最大容量。请求到来时需从桶中获取令牌,有令牌则可继续处理,无令牌则请求被限流。比如设定桶容量为100个令牌,每秒生成10个令牌,若请求瞬间到来200个,只能处理100个,剩余100个等待新令牌生成。
- 漏桶算法:请求像水一样注入漏桶,漏桶以固定速率流出水(处理请求),若注入速度过快,桶满后多余请求会被丢弃。可想象一个底部有小孔的桶,水不断注入但从小孔恒定流出,水注入太快就会溢出。
- 应用场景:在高并发网络请求场景中,防止服务器因请求过多负载过高。如Web服务器对API接口请求进行限流,保护服务器稳定运行。
第三方库
- p-limit:轻量级Promise并发控制库。可设置最大并发数,简单易用。示例代码:
javascript
const limit = require('p-limit')(2); // 设置最大并发数为2
const tasks = [
() => new Promise((resolve) => setTimeout(() => { console.log('Task 1'); resolve(); }, 1000)),
() => new Promise((resolve) => setTimeout(() => { console.log('Task 2'); resolve(); }, 1500)),
() => new Promise((resolve) => setTimeout(() => { console.log('Task 3'); resolve(); }, 2000))
];
const runTasks = async () => {
const results = await Promise.all(tasks.map(task => limit(task)));
};
runTasks();
- async - pool:支持多种并发策略的Promise并发控制库,能灵活控制并发任务数量、处理任务队列等 。
番外
如何在前端实现文件的断点续传,并确保大文件安全可靠上传?
回答重点
为了在前端实现文件的断点续传,并确保大文件能够安全可靠地上传,我们需要以下关键技术和步骤:
- 文件分块上传 (Chunked Upload) :将大文件分成多个小块,每个小块可以独立上传,这样即便在上传过程中网络中断,我们也只需要重新上传未完成的小块,而不必重新上传整个文件。
- 断点续传标识 (Resume Identifier) :为了实现断点续传,我们需要一个唯一的标识符来标记已经上传的分块。通常可以通过文件的名字、大小和哈希值生成这样一个标识符。
- MD5 校验 (MD5 Checksum) :在上传每个分块之后,计算其 MD5 校验值,并在服务器端进行校验,确保分块在传输过程中没有被篡改或损坏。
- 并发上传 (Concurrent Uploads) :利用浏览器的并发上传能力,可以同时上传多个分块,提高上传速度。
- 进度监控 (Progress Monitoring) :利用 XMLHttpRequest 或 Fetch API 的进度事件,可以实时跟踪上传进度,并在前端界面上显示。
扩展知识
- 文件分块实现:我们可以利用 JavaScript 的 File 对象和 Blob.prototype.slice 方法将文件分成多个小块。例如:
javascript
const chunkSize = 5 * 1024 * 1024; // 每块5MB
const file = document.getElementById('fileInput').files[0];
const totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const chunk = file.slice(start, end);
// 这里可以执行上传操作
}
- 断点续传标识生成:通过文件的名字、大小和哈希值生成唯一标识符。例如:
javascript
function generateIdentifier(file) {
return `${file.name}-${file.size}-${file.lastModified}`;
}
- MD5 校验:使用 js-md5 库或 Web Cryptography API 实现分块的 MD5 校验:
javascript
async function calculateMD5(fileChunk) {
const arrayBuffer = await fileChunk.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('MD5', arrayBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
- 并发上传:通过 Promise.all 同时上传多个分块提高上传速度:
javascript
const promises = [];
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
const promise = uploadChunk(chunk);
promises.push(promise);
}
await Promise.all(promises);
- 进度监控:使用 XMLHttpRequest 或 Fetch API 的进度事件,可以更新前端界面的上传进度条。例如:
javascript
function uploadChunk(chunk) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Chunk upload: ${percentComplete}% complete`);
}
});
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error('Chunk upload failed'));
}
};
xhr.send(chunk);
});
}
怎么用 JS 实现大型文件上传?要考虑哪些问题?
在前端实现大型文件上传,需要考虑以下几个问题:
- 分片上传:将大文件切割成多个小块进行上传,可以避免一次性上传大文件导致的上传时间过长,网络中断等问题。通常情况下,每个块大小为 1MB 左右。
- 断点续传:由于网络等因素,上传过程中可能出现中断,此时需要能够从中断的地方恢复上传。
- 并发上传:多个文件同时上传,需要对上传队列进行管理,保证上传速度和顺序。
- 上传进度显示:及时显示上传进度,让用户知道上传进度和状态。
可以通过使用第三方库来实现大型文件上传,比如 Plupload、Resumable.js 等。
以下是一个使用 Plupload 实现大型文件上传的示例:
html
<!-- 引入 Plupload 的 JavaScript 和 CSS 文件 -->
<script type="text/javascript" src="plupload.full.min.js"></script>
<link rel="stylesheet" href="plupload.css">
<!-- 上传控件的容器 -->
<div id="uploader">
<p>Your browser doesn't have Flash, Silverlight or HTML5 support.</p>
</div>
<!-- 初始化上传控件 -->
<script type="text/javascript">
var uploader = new plupload.Uploader({
browse_button: 'uploader', // 上传控件的容器
url: '/upload', // 上传文件的 URL
multi_selection: false, // 是否允许同时上传多个文件
filters: {
max_file_size: '100mb', // 最大上传文件大小
mime_types: [
{ title: 'Image files', extensions: 'jpg,jpeg,gif,png' },
{ title: 'Zip files', extensions: 'zip,rar' }
]
},
init: {
// 添加文件到上传队列之前触发的事件
BeforeUpload: function (up, file) {
console.log('BeforeUpload:', file.name);
},
// 开始上传文件时触发的事件
UploadFile: function (up, file) {
console.log('UploadFile:', file.name);
},
// 上传进度改变时触发的事件
UploadProgress: function (up, file) {
console.log('UploadProgress:', file.percent);
},
// 上传成功时触发的事件
FileUploaded: function (up, file, info) {
console.log('FileUploaded:', file.name, info.response);
},
// 上传出错时触发的事件
Error: function (up, err) {
console.log('Error:', err.message);
}
}
});
// 初始化上传控件
uploader.init();
</script>
在后端,需要根据上传控件发送的请求,来实现文件的接收和存储。具体实现方式视具体情况而定,可以使用 SpringMVC、Express.js 等框架来实现。同时,也需要考虑上传文件大小限制、上传速度控制等问题。