Spring Boot + MinIO 文件分片上传/断点续传/秒传

2025博客之星年度评选已开启 10w+人浏览 2.7k人参与

Spring Boot + MinIO 文件分片上传/断点续传/秒传

  通过这篇文章你将了解到 Spring Boot + Minio 如何实现文件分片上传、断点续传、秒传功能、失败重试、并发控制、进度显示等功能,包括设计原理、流程说明、代码示例、注意事项、扩展点、优化点等。


—— 2025 年 12 月 29 日 乙巳蛇年十一月初十
扫码关注微信公众号   了解最新最全文章

qrcode

前言

  在文件上传场景中,你是否会遇到这些问题:文件太大,几GB甚至几十GB,上传需要花费好几个小时;眼看着已经上传百分之九十九了,结果由于网络波动/页面关闭/系统退出等问题导致上传失败了;可能已经上传了某个文件,但你不知道,又重新上传了一遍,小文件还好,大文件那又得花好长时间。这些问题严重影响着用户体验,所以就有了 分片上传、断点续传、秒传 等设计。

1 流程设计

1.1 分片上传

fenpianshangchuan

  如上图所示,文件分片上传的详细流程如下:

  • 1、用户选择文件。
  • 2、前端计算文件的 md5 值。md5 值可用于验证文件的完整性,前后端用相同的 md5 对称加密算法对同一文件进行加密,然后比较各自产生的 md5 值是否相等,若想等则说明文件未被篡改,反之亦然。注意,只有改变文件内容才能改变 md5 值,改变文件名不会改变 md5 值。
  • 3、前端调用 初始化上传接口。该接口的主要作用是根据 md5 值判断文件是否已上传,或已上传了部分。
  • 4、在 初始化上传接口 中,根据 md5 尝试从 redis 中获取文件 URL,若不为空则直接返回给前端;若为空,则分两种情况:一种是该文件从未被上传过,则创建上传任务(如生成任务唯一标识 uploadId 、根据文件大小与分片大小计算总分片数等)并返回给前端;二是该文件被上传过,但只上传了部分属于断点续传逻辑(这一小节只说明正常的分片上传逻辑,断点续传见下小节)。
  • 5、前端判断 URL 是否为空。若不为空,则说明文件已上传,直接显示上传成功即可(实际上这就是秒传的逻辑);若为空则走正常分片上传逻辑或断点续传逻辑。
  • 6、前端根据后端返回的分片大小和总分片数将文件分片,并调用 上传分片接口 逐个上传分片。
  • 7、在 上传分片接口 中,后端将分片上传至文件服务的临时目录下,然后将 redis 中这个分片的状态更新为已上传并记录分片索引 chunkIndex
  • 8、每个分片上传成功后,前端需要更新上传进度条。
  • 9、所有分片都上传完成后,调用 完成上传/合并请求接口
  • 10、在 完成上传/合并请求接口 接口中,将所有分片合并为原始文件并保存至最终目录下(目标目录),删除临时目录下的所有分片,保存该文件 md5 到文件访问地址 URL 的映射,然后返回 URL。
  • 11、上传成功。

1.2 断点续传

duandianxuchaun

  如上图所示,文件断点续传的详细流程如下:

  • 1、用户重新上传文件。
  • 2、前端计算文件 md5 值。
  • 3、前端调用 初始化上传接口
  • 4、若 初始化上传接口 返回内容不为空但文件访问 URL 为空时,说明该文件已经上传了部分,此时只需要上传剩余部分即可。且该接口会返回哪些分片(分片索引 chunkIndex) 上传了。
  • 5、前端计算剩余分片,并调用 上传分片接口 逐个上传剩余分片。
  • 6、后续流程与上述正常分片上传流程一致。

1.3 秒传

  秒传是指当某个文件已经被上传至文件服务器,再被上传时系统直接返回该文件访问地址即可,不用在耗费时间重新上传了。而判断是否为同一文件的依据则是文件的 md5 值,只要文件内容没被修改过,那么文件的 md5 值就不会变。如果同一文件前后端生成了不同的 md5 值,那说明文件被篡改了。

2 后端编码设计

  后端主要负责上传任务信息的维护、通过 MinioClientMinIO 服务端的交互。故我们需要设计一个上传任务管理器类 UploadTaskManager ,上传任务信息存储在 redis 中,所以需要对 redis 进行配置 RedisConfig ,以及封装的操作 redis 的 RedisUtils 类;我们需要在后端通过 MinioClientMinIO 服务端进行交互,所以需要设计一个 MinioService 类来封装关于 MinIO 的操作,同时需要对 MinioClient 进行配置 MinioConfig ;除了这些核心类之外,还需要设计处理业务操作的 FileToMinioService 类与和前端进行交互的 FileToMinioController 控制器。

2.1 核心依赖

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- minio SDK -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.5.7</version>
        </dependency>

2.2 相关配置

################### spring 配置 ###################
spring:
  application:
    name: file-server
  profiles:
    active: dev
  servlet:
    multipart:
      max-file-size: 100GB
      max-request-size: 100GB
      enabled: true

################### minio 配置 ###################
minio:
  endpoint: http://localhost:9000   # minio 服务端地址
  access-key: ******
  secret-key: ******
  bucket-name: momo   # 文件目标桶名
  temp-bucket: momo-temp   # 分片临时桶名
  chunk-size: 5242880   # 分片大小 默认 5MB 可由前端通过参数指定
  upload-timeout: 7200   # 上传超时时间 秒 2h

2.3 实体类设计

2.3.1 上传任务信息实体
@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UploadTask implements Serializable {

    @Serial
    private static final long serialVersionUID = -7351326905892114331L;

    // 上传任务 id
    private String uploadId;

    // 文件名
    private String filename;

    // 文件 md5
    private String fileMd5;

    // 文件大小
    private Long fileSize;

    // 总分片数
    private Integer totalChunks;

    // 上传状态:0:上传中;1:已完成;2:失败
    private Integer status;

    // 文件 url
    private String fileUrl;

    // 已上传分片的索引集合
    private Set<Integer> uploadedChunks = new HashSet<>();

    // 分片索引与 Etag 映射 分片索引 -> Etag
    private Map<Integer, String> chunkEtags = new HashMap<>();
}
2.3.2 初始化任务返回值
@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InitUploadResponse implements Serializable {

    @Serial
    private static final long serialVersionUID = -5309973156739630192L;

    // 上传任务 id
    private String uploadId;

    // 文件 url(秒传时返回)
    private String fileUrl;

    // 总分片数
    private Integer totalChunks;

    // 是否秒传
    private Boolean quickUpload;

    // 已上传分片的索引列表
    private Set<Integer> uploadedChunks;
}

2.4 控制器业务处理

2.4.1 控制器
@Tag(name = "文件上传至 Minio")
@RestController
@RequestMapping("/fileToMinio")
public class FileToMinioController {

    private final FileToMinioService fileToMinioService;

    public FileToMinioController(FileToMinioService fileToMinioService) {
        this.fileToMinioService = fileToMinioService;
    }

    @Operation(summary = "初始化上传")
    @Parameters({
            @Parameter(name = "filename", description = "文件名(带后缀)", required = true),
            @Parameter(name = "fileMd5", description = "文件 MD5 值", required = true),
            @Parameter(name = "fileSize", description = "文件大小", required = true),
            @Parameter(name = "chunkSize", description = "分片大小")
    })
    @PostMapping(value = "/initUpload")
    public APIResponse<InitUploadResponse> initUpload(@RequestParam("filename") String filename,
                                                      @RequestParam("fileMd5") String fileMd5,
                                                      @RequestParam("fileSize") Long fileSize,
                                                      @RequestParam(name = "chunkSize", required = false) Integer chunkSize) {
        if (!StringUtils.hasText(filename) || !StringUtils.hasText(fileMd5) || fileSize == null) {
            throw GlobalException.error("参数不能为空!");
        }

        return APIResponse.success(this.fileToMinioService.initUpload(filename, fileMd5, fileSize, chunkSize));
    }

    @Operation(summary = "上传分片")
    @Parameters({
            @Parameter(name = "uploadId", description = "上传任务 id", required = true),
            @Parameter(name = "chunkIndex", description = "分片索引", required = true),
            @Parameter(name = "chunk", description = "分片文件", required = true, schema = @Schema(type = "string", format = "binary")),
    })
    @PostMapping(value = "/uploadChunk")
    public APIResponse<Map<String, Object>> uploadChunk(@RequestParam("uploadId") String uploadId,
                                                        @RequestParam("chunkIndex") int chunkIndex,
                                                        @RequestParam("chunk") MultipartFile chunk) {
        if (!StringUtils.hasText(uploadId) || chunk == null) {
            throw GlobalException.error("参数不能为空!");
        }

        return APIResponse.success(this.fileToMinioService.uploadChunk(uploadId, chunkIndex, chunk));
    }

    @Operation(summary = "完成上传")
    @Parameter(name = "uploadId", description = "上传任务 id", required = true)
    @PostMapping("/completeUpload")
    public APIResponse<Map<String, Object>> completeUpload(@RequestParam("uploadId") String uploadId) {
        this.checkParams(uploadId);
        return APIResponse.success(this.fileToMinioService.completeUpload(uploadId));
    }

    @Operation(summary = "获取上传任务(包括进度)")
    @Parameter(name = "uploadId", description = "上传任务 id", required = true)
    @PostMapping("/getUploadTask")
    public APIResponse<UploadTask> getUploadTask(@RequestParam("uploadId") String uploadId) {
        this.checkParams(uploadId);
        return APIResponse.success(this.fileToMinioService.getUploadTask(uploadId));
    }

    @Operation(summary = "取消上传")
    @Parameter(name = "uploadId", description = "上传任务 id", required = true)
    @PostMapping("/cancelUpload")
    public APIResponse<Object> cancelUpload(@RequestParam("uploadId") String uploadId) {
        this.checkParams(uploadId);
        this.fileToMinioService.cancelUpload(uploadId);
        return APIResponse.success();
    }

    // 参数校验
    private void checkParams(String uploadId) {
        if (!StringUtils.hasText(uploadId)) {
            throw GlobalException.error("参数不能为空!");
        }
    }
}
2.4.2 业务实现
@Service
public class FileToMinioServiceImpl implements FileToMinioService {

    private final UploadTaskManager uploadTaskManager;

    private final MinioService minioService;

    @Value("${minio.chunk-size}")
    private Integer chunkSize;

    public FileToMinioServiceImpl(UploadTaskManager uploadTaskManager, MinioService minioService) {
        this.uploadTaskManager = uploadTaskManager;
        this.minioService = minioService;
    }

  	// 初始化上传任务
    @Override
    public InitUploadResponse initUpload(String filename, String fileMd5, Long fileSize, Integer chunkSize) {

        InitUploadResponse response = InitUploadResponse.builder().build();

      	// 检查是否上传过
        String fileUrl = this.uploadTaskManager.getFileUrlByMd5(fileMd5);
        if (StringUtils.hasText(fileMd5)) {
            response.setFileUrl(fileUrl);
            response.setQuickUpload(true);
            return response;
        }

      	// 计算分片总数
        int totalChunks = (int) Math.ceil((double) fileSize / this.determineChunkSize(chunkSize));

      	// 初始化上传任务
        UploadTask uploadTask = this.uploadTaskManager.createUploadTask(filename, fileMd5, fileSize, totalChunks);

        response.setUploadId(uploadTask.getUploadId());
        response.setTotalChunks(totalChunks);
        response.setQuickUpload(false);
        response.setUploadedChunks(uploadTask.getUploadedChunks());

        return response;
    }
	
  	// 上传分片
    @Override
    public Map<String, Object> uploadChunk(String uploadId, int chunkIndex, MultipartFile chunk) {

        this.checkUploadTask(uploadId);

      	// 这里可加入重试机制 提高成功率 如使用 spring-retry 组件
        String etag = this.minioService.uploadChunk(uploadId, chunkIndex, chunk);

      	// 标记当前分片已上传
        this.uploadTaskManager.markChunkUploaded(uploadId, chunkIndex, etag);

        Map<String, Object> map = new HashMap<>();
        map.put("chunkIndex", chunkIndex);
        map.put("etag", etag);

        return map;
    }

  	// 完成上传(合并分片)
    @Override
    public Map<String, Object> completeUpload(String uploadId) {
        UploadTask uploadTask = this.checkUploadTask(uploadId);

      	// 校验所有分片是否均已上传
        if (!this.uploadTaskManager.isAllChunkUploaded(uploadId)) {
            throw GlobalException.error("存在未上传完成分片!");
        }

      	// 合并分片
        String fileUrl = this.minioService.mergeChunks(uploadTask.getFilename(), uploadId, uploadTask.getTotalChunks());

      	// 更新上传任务为已完成
        this.uploadTaskManager.completeUploadTask(uploadId, fileUrl);

        Map<String, Object> map = new HashMap<>();
        map.put("filename", uploadTask.getFilename());
        map.put("fileUrl", fileUrl);

        return map;
    }

  	// 获取上传任务信息
    @Override
    public UploadTask getUploadTask(String uploadId) {
        return this.checkUploadTask(uploadId);
    }

  	// 取消上传
    @Override
    public void cancelUpload(String uploadId) {
        this.uploadTaskManager.removeUploadTask(uploadId);
    }

    // 校验上传任务
    private UploadTask checkUploadTask(String uploadId) {
        UploadTask uploadTask = this.uploadTaskManager.getUploadTask(uploadId);
        if (uploadTask == null) {
            throw GlobalException.error("上传任务不存在!");
        }

        return uploadTask;
    }

    // 推断分片大小 若未传则使用默认配置(5MB)
    private int determineChunkSize(Integer chunkSize) {
        return chunkSize != null ? chunkSize : this.chunkSize;
    }
}

2.5 组件配置

2.5.1 Redis 配置
@Configuration
public class RedisConfig {

    private final ObjectMapper objectMapper;

    public RedisConfig(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // 使用 String 来序列化 redis 的 key 值
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 使用 Jackson 来序列化和反序列化 redis 的 value 值(默认使用 JDK 的序列化方法)
        RedisSerializer<Object> jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jsonRedisSerializer);

        redisTemplate.setHashKeySerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
2.5.2 MinIO 配置
@Configuration
public class MinioConfig {

    @Value("${minio.endpoint}")
    private String endpoint;

    @Value("${minio.access-key}")
    private String accessKey;

    @Value("${minio.secret-key}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(this.endpoint)
                .credentials(this.accessKey, this.secretKey)
                .build();
    }
}

2.6 核心类

2.6.1 上传任务管理器
@Component
public class UploadTaskManager {

  	// md5 与 url 映射关系 key 前缀
    private static final String FILE_MD5_PREFIX = "file:md5:";

  	// 上传任务信息 key 前缀 值为 UploadTask 对象
    private static final String FILE_TASK_PREFIX = "file:task:";

    private final ObjectMapper objectMapper;

    @Value("${minio.upload-timeout}")
    private Long uploadTimeout;

    public UploadTaskManager(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    // 创建上传任务
    public UploadTask createUploadTask(String filename, String fileMd5, Long fileSize, int totalChunks) {
        UploadTask uploadTask = UploadTask.builder()
                .uploadId(UUID.randomUUID().toString())
                .filename(filename)
                .fileMd5(fileMd5)
                .fileSize(fileSize)
                .totalChunks(totalChunks)
                .status(0)
                .build();

        this.saveUploadTask(uploadTask);

        return uploadTask;
    }

    // 保存上传任务
    public void saveUploadTask(UploadTask uploadTask) {
        String key = FILE_TASK_PREFIX + uploadTask.getUploadId();
        RedisUtils.addValue(key, uploadTask, this.uploadTimeout, TimeUnit.SECONDS);
    }

    // 标记分片已上传
    public void markChunkUploaded(String uploadId, int chunkIndex, String eTag) {
        UploadTask uploadTask = this.getUploadTask(uploadId);
        if (uploadTask == null) {
            throw GlobalException.error("上传任务不存在!");
        }

        uploadTask.getUploadedChunks().add(chunkIndex);
        uploadTask.getChunkEtags().put(chunkIndex, eTag);

        this.saveUploadTask(uploadTask);
    }

    // 完成上传
    public void completeUploadTask(String uploadId, String fileUrl) {
        UploadTask uploadTask = this.getUploadTask(uploadId);
        if (uploadTask == null) {
            throw GlobalException.error("上传任务不存在!");
        }

        uploadTask.setStatus(1);
        uploadTask.setFileUrl(fileUrl);

        this.saveUploadTask(uploadTask);

        this.saveMd5UrlMapping(uploadTask.getFileMd5(), fileUrl);
    }

    // 获取上传任务
    public UploadTask getUploadTask(String uploadId) {
        String key = FILE_TASK_PREFIX + uploadId;
        Object value = RedisUtils.getValue(key);
        if (value == null) {
            return null;
        }

        if (value instanceof UploadTask) {
            return ((UploadTask) value);
        }

        return this.objectMapper.convertValue(value, UploadTask.class);
    }

    // 检查所有分片是否都已上传
    public boolean isAllChunkUploaded(String uploadId) {
        UploadTask uploadTask = this.getUploadTask(uploadId);
        if (uploadTask == null) {
            return false;
        }

        return uploadTask.getTotalChunks().equals(uploadTask.getUploadedChunks().size());
    }

    // 根据文件 md5 获取文件 url
    public String getFileUrlByMd5(String md5) {
        Object value = RedisUtils.getValue(FILE_MD5_PREFIX + md5);
        return value != null ? value.toString() : null;
    }

    // 保存映射:md5 -> 文件 url
    public void saveMd5UrlMapping(String fileMd5, String fileUrl) {
        RedisUtils.addValue(FILE_MD5_PREFIX + fileMd5, fileUrl, 30, TimeUnit.DAYS);
    }

    // 移除上传任务
    public void removeUploadTask(String uploadId) {
        RedisUtils.deleteKey(FILE_MD5_PREFIX + uploadId);
        RedisUtils.deleteKey(FILE_TASK_PREFIX + uploadId);
    }
}
2.6.2 MinIO 服务
@Slf4j
@Service
public class MinioService {

    private final MinioClient minioClient;

    @Value("${minio.bucket-name}")
    private String bucketName;

    @Value("${minio.temp-bucket}")
    private String tempBucket;

    public MinioService(MinioClient minioClient) {
        this.minioClient = minioClient;
    }

    // 初始化桶
    @PostConstruct
    public void initBucket() {
        try {
            this.createBucketIfNotExists(this.bucketName);
            this.createBucketIfNotExists(this.tempBucket);
        } catch (Exception e) {
            throw GlobalException.error("初始化桶失败!", e);
        }
    }

    // 上传分片
    public String uploadChunk(String uploadId, int chunkIndex, MultipartFile file) {
        String objectName = String.format("%s/chunk_%d", uploadId, chunkIndex);

        try {
            ObjectWriteResponse response = this.minioClient.putObject(PutObjectArgs.builder()
                    .bucket(this.tempBucket)
                    .object(objectName)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());

            return response.etag();
        } catch (Exception e) {
            log.error("上传分片失败:bucketName = {}, uploadId = {}, chunkIndex = {},error = {}",
                    this.tempBucket, uploadId, chunkIndex, e);
            throw GlobalException.error("上传分片失败!");
        }
    }

    // 合并分片
    public String mergeChunks(String filename, String uploadId, int totalChunks) {
        String objectName = this.generateObjectName(filename, uploadId);

        List<ComposeSource> sources = new ArrayList<>();
        for (int i = 0; i < totalChunks; i++) {
            sources.add(ComposeSource.builder()
                    .bucket(this.tempBucket)
                    .object(String.format("%s/chunk_%d", uploadId, i))
                    .build());
        }

        try {
            this.minioClient.composeObject(ComposeObjectArgs.builder()
                    .bucket(this.bucketName)
                    .object(objectName)
                    .sources(sources)
                    .build());
        } catch (Exception e) {
            log.error("合并分片失败:bucketName = {}, uploadId = {}, totalChunks = {},error = {}",
                    this.tempBucket, uploadId, totalChunks, e);
            throw GlobalException.error("合并分片失败!");
        }

        this.deleteChunks(uploadId, totalChunks);

        return this.getFileUrl(objectName);
    }

    // 获取文件访问 URL
    public String getFileUrl(String objectName) {
        try {
            return this.minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .bucket(this.bucketName)
                    .object(objectName)
                    .expiry(7, TimeUnit.DAYS)
                    .method(Method.GET)
                    .build());
        } catch (Exception e) {
            log.error("获取文件 URL 失败:bucketName = {}, objectName = {},,error = {}",
                    this.tempBucket, objectName, e);
            throw GlobalException.error("获取文件 URL 失败!");
        }
    }

    // 桶不存在时创建桶
    private void createBucketIfNotExists(String bucketName) throws Exception {
        boolean exists = this.minioClient.bucketExists(BucketExistsArgs.builder()
                .bucket(bucketName)
                .build());

        if (exists) {
            return;
        }

        this.minioClient.makeBucket(MakeBucketArgs.builder()
                .bucket(bucketName)
                .build());
    }

    // 删除临时分片
    private void deleteChunks(String uploadId, int totalChunks) {
        try {
            for (int i = 0; i < totalChunks; i++) {
                this.minioClient.removeObject(RemoveObjectArgs.builder()
                        .bucket(this.tempBucket)
                        .object(String.format("%s/chunk_%d", uploadId, i))
                        .build());
            }
        } catch (Exception e) {
            log.error("删除临时分片失败:bucketName = {}, uploadId = {}, totalChunks = {},error = {}",
                    this.tempBucket, uploadId, totalChunks, e);
            throw GlobalException.error("删除临时分片失败!");
        }
    }

    // 生成对象名
    private String generateObjectName(String fileName, String uploadId) {
        String extension = "";
        int dotIndex = fileName.lastIndexOf(".");
        if (dotIndex > 0) {
            extension = fileName.substring(dotIndex);
        }

        return uploadId + extension;
    }
}

3 前端编码设计

  前端需要对文件进行分片;计算文件 md5 值;为避免过多占用 web 服务器线程以影响其它业务需要进行一定的并发控制,如只限制三个线程处理上传任务;为避免网络波动影响,还可以配置重试机制,建议采用指数退避算法。因篇幅原因,此处只给出相关核心代码。

3.1 文件分片算法

const chunkSize = 5 * 1024 * 1024; // 5MB
const totalChunks = Math.ceil(file.size / chunkSize);

for (let i = 0; i < totalChunks; i++) {
  const start = i * chunkSize;
  const end = Math.min(start + chunkSize, file.size);
  const chunk = file.slice(start, end);
  // 上传 chunk
}

3.2 文件 MD5 值计算

// 使用 SparkMD5 增量计算,避免大文件内存溢出
const spark = new SparkMD5.ArrayBuffer();
// 分块读取文件
spark.append(arrayBuffer);   // 每读一块就 append 到 spark
const md5 = spark.end();   // 最后获取 MD5

3.3 并发控制

// 使用 Promise.all + 工作队列
const concurrency = 3;
const queue = [...chunks];
const workers = [];

for (let i = 0; i < concurrency; i++) {
  workers.push(uploadWorker(queue));
}

await Promise.all(workers);

3.4 重试机制

// 指数退避重试
let retryCount = 0;
const maxRetry = 3;

while (retryCount < maxRetry) {
  try {
    await uploadChunk();
    break;
  } catch (error) {
    retryCount++;
    await sleep(1000 * retryCount); // 1s, 2s, 3s
  }
}

4 优化与扩展

  优化与扩展方面可针对前端、后端和存储进行性能、安全、可用性等方面的优化。

  • Web Worker:MD5 计算在 Worker 中进行,不阻塞 UI。
  • 并发上传与控制:并发上传是指后端可通过线程池(如 Executors.newFixedThreadPool(10) )并行上传分片,以提高效率;并发控制是指前端控制分片上传的并发数,以避免占用过多的 web 服务器线程,从而影响到其他业务。
  • Blob 分片:文件分片时可采用 File.slice() 避免内存占用。
  • 预签名 URL:为减轻服务器压力同时又确保安全性,可使用 MinIO 的预签名 URL。预签名 URL 是一种包含间弥签名和过期时间的临时访问链接。
  • 合理设置分片大小:分片大小需要合理设置,太小会导致协调开销大,太大则降低并行粒度以及重复代价, 5~8MB 是最优解。
  • 临时桶和最终桶:为防止业务影响,可将分片放在临时桶,合并后再放入最终桶(合并后记得删除临时桶里的分片哦)。

qrcode

扫码关注微信公众号   了解最新最全文章
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红衣女妖仙

行行好,给点吃的吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值