禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 项目编排

  • 场景设计

    • Java实现文件分片上传
      • 为什么需要文件分片上传
      • (实践)分片上传
        • 设计思路概述
      • 分片上传表设计
        • 接口设计
        • 分片上传接口实现思路
        • 后端代码
      • (完善)断点续传和极速妙传
        • 极速妙传设计思路
        • 后端代码实现
      • 对接,前端代码实现
      • 常见问题
        • 能不能给我介绍一下MultipartFile
        • base64又是什么类型的数据
        • 能不能基于上述接口给我会绘制一张流程图
        • 关于文件分片上传有没有考虑过OSS
        • 给我说说你们文件上传的场景吧
        • 你觉得你这个设计还有没有提高的空间
        • 如果需要进行并发上传的话
      • 参考文献
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI-CD

  • 架构设计

  • 监控方法论

  • 软件工程实践
  • 场景设计
sharkchili
2022-12-31
目录

Java实现文件分片上传

# 为什么需要文件分片上传

  1. 大文件上传中断:假如我们有一个5G的文件,上传过程中突然中断我们该怎么办?
  2. 上文件上传响应时间长:假如我们有个10G的文件,单次上传时间长,用户体验长,该怎么办?
  3. 大文件上传重复上传:某些大文件,我们已经上传过了,我们不想再一次上传,该怎么办?

# (实践)分片上传

# 设计思路概述

如下图,我们会将一个大文件进行切片,然后调用文件上传接口,将分片base64数据、源文件名称、分片大小、分片个数、索引号传到服务器上。

在这里插入图片描述

# 分片上传表设计

为了保存分片上传进度,笔者创建了下面这样一张表,这张表记录了上传的文件以及文件分片上传的进度。 当我们上传的分片是第一个分片时,我们就会插入一条数据,记录文件名、分片索引号等信息。 后续上传成功的分片则都是更新信息shard_index这个字段。

DROP TABLE IF EXISTS `file`;
CREATE TABLE `file` (
  `id` char(8) NOT NULL DEFAULT '' COMMENT 'id',
  `path` varchar(100) NOT NULL COMMENT '相对路径',
  `name` varchar(100) DEFAULT NULL COMMENT '文件名',
  `suffix` varchar(10) DEFAULT NULL COMMENT '后缀',
  `size` int(11) DEFAULT NULL COMMENT '大小|字节B',
  `use` char(1) DEFAULT NULL COMMENT '用途|枚举[FileUseEnum]:COURSE("C", "讲师"), TEACHER("T", "课程")',
  `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime(3) DEFAULT NULL COMMENT '修改时间',
  `shard_index` int(11) DEFAULT NULL COMMENT '已上传分片',
  `shard_size` int(11) DEFAULT NULL COMMENT '分片大小|B',
  `shard_total` int(11) DEFAULT NULL COMMENT '分片总数',
  `key` varchar(32) DEFAULT NULL COMMENT '文件标识',
  PRIMARY KEY (`id`),
  UNIQUE KEY `path_unique` (`path`),
  UNIQUE KEY `key_unique` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 接口设计

注意,以下参数都是必传参数:

  1. name:文件名称(包含文件后缀)。
  2. shard:文件base64值。
  3. size:文件总大小。
  4. shardTotal:文件总分片数。
  5. shardSize:每个分片最大值,注意这里笔者说的分片最大值,原因很简单,如果我们文件分片大小。为10M,很可能最后一个分片大小不足10M,所以这个参数传的就是切割分片后的最大值。
  6. suffix:文件后缀,视频后缀为mp4,图片则为jpg等。

# 分片上传接口实现思路

  1. 必传参数校验。
  2. 获取文件base64值,并转为MultipartFile。
  3. 获取本地存储路径,并判断该路径是否存在,如果不存在,则创建路径。
  4. 根据分片索引创建本地分片全路径,将MultipartFile写到这个路径中。
  5. 写入完成,记录文件上传进度,若这个文件之前都没有上传过则插入一条新数据,反之更新索引字段那一栏的数据。
  6. 判断本次上传的图片的索引号是否和文件总大小一致,如果一致说明上传完成开始进行文件合并,合并完成后,删除临时分片。

# 后端代码

在进行分片上传逻辑编写前,我们必须配置一下本地存储路径和映射地址,如下所示,笔者将file.path设置为本地文件存储路径。将file.domain设置为file.path对外的映射地址。

file.path=F:/video/
file.domain=http://127.0.0.1:9000/file/f/
1
2

为了做到这一点,笔者创建了一个SpringMvcConfig 配置,这样以来,我们在浏览器中键入http://127.0.0.1:9000/file/f/1.jpg就相当于访问文件服务器的F:/video/1.jpg

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {


    @Value("${file.path}")
    private String FILE_PATH;


   

    /**
     * 文件查看的映射
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/f/**").addResourceLocations("file:"+FILE_PATH);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

完成这个步骤之后我们就可以开始编写后端代码了,为了方便读者理解,笔者基于上述编码思路一段一段讲述代码实现思路

首先是参数校验,这一步无非是判空,所以笔者就补贴出完整代码,这里就把方法贴出来让读者了解一下即可 。

  //1. 参数非空检查
        checkParams(fileDto);
1
2

参数校验无误后,将base64转为MultipartFile

//将base64转MultipartFile
MultipartFile multipartFile = Base64ToMultipartFile.base64ToMultipart(fileDto.getShard());
1
2

这个工具类实现思路也很简单Base64ToMultipartFile,根据base64封号切割成数组,数组[0]是base64的描述信息,数组[1]则是具体内容,进行转码组装,具体参见下属代码:

public class Base64ToMultipartFile implements MultipartFile {

    private final byte[] imgContent;
    private final String header;

    public Base64ToMultipartFile(byte[] imgContent, String header) {
        this.imgContent = imgContent;
        this.header = header.split(";")[0];
    }

   ......略

    @Override
    public void transferTo(File dest) throws IOException, IllegalStateException {
        new FileOutputStream(dest).write(imgContent);
    }

    public static MultipartFile base64ToMultipart(String base64) {
        try {
            String[] baseStrs = base64.split(",");

            BASE64Decoder decoder = new BASE64Decoder();
            byte[] b = new byte[0];
            b = decoder.decodeBuffer(baseStrs[1]);

            for(int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    b[i] += 256;
                }
            }

            return new Base64ToMultipartFile(b, baseStrs[0]);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

base64数据结构如下图所示:

在这里插入图片描述

获取本地存储路径,若不存在则创建

 //获取本地文件夹地址,拼接传入的参数use,得到一个本地文件夹路径,并判断是否存在,若不存在则直接创建
        String localDirPath = FILE_PATH + FileUseEnum.getByCode(fileDto.getUse());
        logger.info("本地文件夹地址:{}", localDirPath);
        File dirFile = new File(localDirPath);

        //如果目标文件夹不存在,则直接创建一个
        if (!dirFile.exists() && !dirFile.mkdirs()) {
            logger.error("文件夹创建失败,待创建路径:{}", localDirPath);
            throw new Exception("文件夹创建失败,创建路径:" + localDirPath);
        }
1
2
3
4
5
6
7
8
9
10

组装文件路径和分片路径,并将文件数据写入到分片路径中。

 //本地文件全路径
        String fileFullPath = localDirPath +
                File.separator +
                fileDto.getKey() +
                "." +
                fileDto.getSuffix();

        //创建文件分片全路径,将multipartFile写入到这个路径中
        String fileShardFullPath = fileFullPath +
                "." +
                fileDto.getShardIndex();

        multipartFile.transferTo(new File(fileShardFullPath));
1
2
3
4
5
6
7
8
9
10
11
12
13

成功完成文件上传后,更新文件信息表上传进入,如果表中没有这个文件的信息,则插入一条数据。反之更新文件信息表分片索引号字段。

  //更新文件表信息,无上传过这个文件则插入一条,反之直接更新索引值
        String relaPath = FileUseEnum.getByCode(fileDto.getUse()) + "/" + fileDto.getKey() + "." + fileDto.getSuffix();
        fileDto.setPath(relaPath);
        fileService.save(fileDto);
1
2
3
4

可以看到save的逻辑如下所示

 /**
     * 如果这个文件之前都没上传过则插入一条数据,反之更新索引只即可
     */
    public void save(FileDto fileDto) {
        File file = CopyUtil.copy(fileDto, File.class);
        File fileDb = selectByKey(fileDto.getKey());
        if (fileDb == null) {
            this.insert(file);
        } else {
            fileDb.setShardIndex(fileDto.getShardIndex());
            this.update(fileDb);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

如果当前上传的分片为最后一个分片,则进行文件合并

 //判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
        if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
            fileDto.setPath(fileFullPath);
            //文件合并
            merge(fileDto);
        }
1
2
3
4
5
6

组装结果并返回

ResponseDto responseDto = new ResponseDto();
        FileDto result = new FileDto();
        //设置文件映射地址给前端
        result.setPath(FILE_DOMAIN + "/" + relaPath);
        responseDto.setContent(result);


        logger.info("文件分片上传结束,请求结果:{}", JSON.toJSONString(responseDto));
        return responseDto;
1
2
3
4
5
6
7
8
9

文件合并逻辑,这里就比较简单了,创建输入流和输出流,以追加的形式将每个分片都写到输出文件中。

private void merge(FileDto fileDto) {
        logger.info("文件分片合并开始,请求参数:{}", JSON.toJSONString(fileDto));
        String path = fileDto.getPath();
        try (OutputStream outputStream = new FileOutputStream(path, true)) {
            for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
                try (FileInputStream inputStream = new FileInputStream(path + "." + i);) {
                    byte[] bytes = new byte[10 * 1024 * 1024];
                    int len;
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                    }
                }

            }
        } catch (Exception e) {
            logger.error("文件合并失败,失败原因:{}", e.getMessage(), e);
        }

		System.gc();
        //删除所有分片
        for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
            File file = new File(path + "." + i);
            file.delete();
        }


    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

注意,这里有时需要考虑一个文件,就是文件合并后无论流如何关闭,在JVM进行gc回收之前,这些文件的资源在进行删除操作时可能会失败,所以稳妥起见,笔者尝试过调用System.gc()这个方法进行兜底。

这种方法不太符合编码规范,所以笔者建议使用定时任务等方式每日按时按点进行删除。

在这里插入图片描述

完整代码

@RequestMapping("/upload")
    public ResponseDto uploadShard(@RequestBody FileDto fileDto) throws Exception {
        logger.info("文件分片上传请求开始,请求参数: {}", JSON.toJSONString(fileDto));
        //1. 参数非空检查
        checkParams(fileDto);


        //将base64转MultipartFile
        MultipartFile multipartFile = Base64ToMultipartFile.base64ToMultipart(fileDto.getShard());


        //获取本地文件夹地址,拼接传入的参数use,得到一个本地文件夹路径,并判断是否存在,若不存在则直接创建
        String localDirPath = FILE_PATH + FileUseEnum.getByCode(fileDto.getUse());
        logger.info("本地文件夹地址:{}", localDirPath);
        File dirFile = new File(localDirPath);

        //如果目标文件夹不存在,则直接创建一个
        if (!dirFile.exists() && !dirFile.mkdirs()) {
            logger.error("文件夹创建失败,待创建路径:{}", localDirPath);
            throw new Exception("文件夹创建失败,创建路径:" + localDirPath);
        }

        //本地文件全路径
        String fileFullPath = localDirPath +
                File.separator +
                fileDto.getKey() +
                "." +
                fileDto.getSuffix();

        //创建文件分片全路径,将multipartFile写入到这个路径中
        String fileShardFullPath = fileFullPath +
                "." +
                fileDto.getShardIndex();

        multipartFile.transferTo(new File(fileShardFullPath));

        //更新文件表信息,无上传过这个文件则插入一条,反之直接更新索引值
        String relaPath = FileUseEnum.getByCode(fileDto.getUse()) + "/" + fileDto.getKey() + "." + fileDto.getSuffix();
        fileDto.setPath(relaPath);
        fileService.save(fileDto);

        //判断当前分片索引是否等于分片总数,如果等于分片总数则执行文件合并
        if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
            fileDto.setPath(fileFullPath);
            //文件合并
            merge(fileDto);
        }

        ResponseDto responseDto = new ResponseDto();
        FileDto result = new FileDto();
        //设置文件映射地址给前端
        result.setPath(FILE_DOMAIN + "/" + relaPath);
        responseDto.setContent(result);


        logger.info("文件分片上传结束,请求结果:{}", JSON.toJSONString(responseDto));
        return responseDto;
    }



 /**
     * 文件合并
     *
     * @param fileDto
     */
    private void merge(FileDto fileDto) {
        logger.info("文件分片合并开始,请求参数:{}", JSON.toJSONString(fileDto));
        String path = fileDto.getPath();
        try (OutputStream outputStream = new FileOutputStream(path, true)) {
            for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
                try (FileInputStream inputStream = new FileInputStream(path + "." + i);) {
                    byte[] bytes = new byte[10 * 1024 * 1024];
                    int len;
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                    }
                }

            }
        } catch (Exception e) {
            logger.error("文件合并失败,失败原因:{}", e.getMessage(), e);
        }


        //删除所有分片
        for (Integer i = 1; i <= fileDto.getShardTotal(); i++) {
            File file = new File(path + "." + i);
            file.delete();
        }


    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

# (完善)断点续传和极速妙传

# 极速妙传设计思路

因为是大文件,我们很可能文件传一半就因为各种原因导致中断,我们的实现思路是和前端约定好为系统的文件都生成一个key,后端用这个key到数据库中查询是否存在上传记录。 如果有结果则结果返回给前端。

然后前端进行如下操作:

  1. 如果返回结果不为空,则记录中已上传索引值等于文件分片数,则说明之前上传过相同的文件,直接告知用户极速妙传成功。
  2. 如果返回结果不为空,已上传的索引值不等于文件分片数,则基于这个索引值+1完成断点续传。

# 后端代码实现

/**
     * 文件上传进度检查接口
     *
     * @param key
     * @return
     */
    @GetMapping("/check/{key}")
    public ResponseDto check(@PathVariable String key) {
        logger.info("文件上传进度检查接口请求开始,请求参数:{}", key);
        if (StringUtils.isEmpty(key)) {
            throw new BusinessException(BusinessExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION);
        }

        FileDto fileDto = fileService.findByKey(key);
        ResponseDto responseDto = new ResponseDto();

        //如果不为空,则返回映射地址以及文件上传进度
        if (fileDto != null) {
            //将文件映射地址告知前端
            fileDto.setPath(FILE_DOMAIN + "/" + fileDto.getPath());
            responseDto.setContent(fileDto);
        }

        return responseDto;

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 对接,前端代码实现

前端文件分片计算代码逻辑(ps:笔者基于Vue写的)

getFileShard (shardIndex, shardSize) {
                let _this = this;
                let file = _this.$refs.file.files[0];
                let start = (shardIndex - 1) * shardSize;	//当前分片起始位置
                let end = Math.min(file.size, start + shardSize); //当前分片结束位置
                let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
                return fileShard;
            },
1
2
3
4
5
6
7
8

调用极速妙传和断点续传的逻辑

/**
             * 检查文件状态,是否已上传过?传到第几个分片?
             */
            check (param) {
                let _this = this;
                _this.$ajax.get(process.env.VUE_APP_SERVER + '/file/admin/check/' + param.key).then((response)=>{
                    let resp = response.data;
                    if (resp.success) {
                        let obj = resp.content;
                        //如果不存在则从第一个分片开始上传
                        if (!obj) {
                            param.shardIndex = 1;
                            console.log("没有找到文件记录,从分片1开始上传");
                            _this.upload(param);
                        } else if (obj.shardIndex === obj.shardTotal) {
                            // 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
                            Toast.success("文件极速秒传成功!");
                            _this.afterUpload(resp);
                            $("#" + _this.inputId + "-input").val("");
                        }  else {
                            param.shardIndex = obj.shardIndex + 1;
                            console.log("找到文件记录,从分片" + param.shardIndex + "开始上传");
                            _this.upload(param);
                        }
                    } else {
                        Toast.warning("文件上传失败");
                        $("#" + _this.inputId + "-input").val("");
                    }
                })
            },

            /**
             * 将分片数据转成base64进行上传
             */
            upload (param) {
                let _this = this;
                let shardIndex = param.shardIndex;
                let shardTotal = param.shardTotal;
                let shardSize = param.shardSize;
                let fileShard = _this.getFileShard(shardIndex, shardSize);
                // 将图片转为base64进行传输
                let fileReader = new FileReader();

                Progress.show(parseInt((shardIndex - 1) * 100 / shardTotal));
                fileReader.onload = function (e) {
                    let base64 = e.target.result;
                    // console.log("base64:", base64);

                    param.shard = base64;

                    _this.$ajax.post(process.env.VUE_APP_SERVER + '/file/admin/upload', param).then((response) => {
                        let resp = response.data;
                        console.log("上传文件成功:", resp);
                        Progress.show(parseInt(shardIndex * 100 / shardTotal));
                        if (shardIndex < shardTotal) {
                            // 上传下一个分片
                            param.shardIndex = param.shardIndex + 1;
                            _this.upload(param);
                        } else {
                            Progress.hide();
                            _this.afterUpload(resp);
                            $("#" + _this.inputId + "-input").val("");
                        }
                    });
                };
                fileReader.readAsDataURL(fileShard);
            },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 常见问题

# 能不能给我介绍一下MultipartFile

答: 这个是Spring框架自带的一个类,便于用户更好操作网络传输的文件,这个类为我们提供了很多便捷操作的API。

getName():获取文件名
getOriginalFilename():返回客户端系统中原始文件名。
getContentType():获取文件内容类型。
isEmpty():判断文件是否为空。
getSize():获取文件大小,以字节为单位。
getBytes():获取文件的字节数组。
getInputStream():获取文件输入流。
transferTo(File dest):将目标文件传输到目标文件中。
transferTo(Path dest) :将目标文件传输到目标地址中。
1
2
3
4
5
6
7
8
9

以笔者为例,笔者为了将前端传入的base64字符串转为MultipartFile,于是继承了MultipartFile编写了一个工具类

这个类首先声明两个成员属性,imgContent记录文件内容,header记录base64文件标识。

 private final byte[] imgContent;
 private final String header;
1
2

获取文件名以及获取文件类型等相关方法的重写

 @Override
    public String getName() {
        // TODO - implementation depends on your requirements
        return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
    }

    @Override
    public String getOriginalFilename() {
        // TODO - implementation depends on your requirements
        return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
    }
@Override
    public String getContentType() {
        // TODO - implementation depends on your requirements
        return header.split(":")[1];
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

重点:base64转MultipartFile 逻辑,如下所示,将base64封号后面的内容转为数组b ,然后将这个数据赋给imgContent,文件描述信息赋给header。

public static MultipartFile base64ToMultipart(String base64) {
        try {
            String[] baseStrs = base64.split(",");

            BASE64Decoder decoder = new BASE64Decoder();
            byte[] b = null;
            b = decoder.decodeBuffer(baseStrs[1]);

            for(int i = 0; i < b.length; ++i) {
                if (b[i] < 0) {
                    b[i] += 256;
                }
            }

			//将文件内容赋值给imgContent,文件描述信息baseStrs[0]赋给header
            return new Base64ToMultipartFile(b, baseStrs[0]);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

 public Base64ToMultipartFile(byte[] imgContent, String header) {
        this.imgContent = imgContent;
        this.header = header.split(";")[0];
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# base64又是什么类型的数据

答: base64是为了在网络传输中避免中文乱码、媒体文件二进制传输数据不可打印等情况诞生的一种编码技术。它的编码过程也很简单,就是将每个字符串的二进制编码值全部合在一起,每6位算一个字符,再配合base64编码表得出最终结果。

base64编码表

在这里插入图片描述

我们就以字符串A为例子,它的ASCII码值为65,那么二进制数值就为:1000001,按照base64的规则,将字符串中24位为一组(不足24位则算做空位)。所以A的二进制值为01 00 00 01 00 00 后12位全空,每6位算出一个十进制的值,根据base64编码表得出最终的值,01 00 00 得到一个Q,01 00 00又得到一个Q,后面全空的6位算一个=,最终结果为QQ==。

在这里插入图片描述

为了让读者加深对base64编码的理解,我们再以字符串Man为例,我们得出它的ASCII码值为77 97 110

它的计算过程如下图所示

在这里插入图片描述

对此我们可以用Java代码来验证一下

 public static void main(String[] args) {
        String man = "Man";
        String a = "A";

        BASE64Encoder encoder = new BASE64Encoder();
        System.out.println("Man base64结果为:" + encoder.encode(man.getBytes()));
        System.out.println("A base64结果为:" + encoder.encode(a.getBytes()));
    }
1
2
3
4
5
6
7
8

输出结果

Man base64结果为:TWFu
A base64结果为:QQ==
1
2

# 能不能基于上述接口给我会绘制一张流程图

在这里插入图片描述

# 关于文件分片上传有没有考虑过OSS

答: 有的,某些场景下我们的项目会使用Amazon S3,相比自建文件服务器,减少了很多运维压力,而且Amazon S3也为我们提供了multipart upload API,最大支持上传5TB的文件。 为了保证扩展,我们也留了一个策略类实现Amazon S3的文件上传。

# 给我说说你们文件上传的场景吧

答: 我们某个系统需要调用另一个服务的RPC接口进行文件上传,由于某些文件属于大文件,所以经常出现文件上传途中导致文件超时等问题,对此我们采用了文件分片上传的方案解决问题超时问题。

# 你觉得你这个设计还有没有提高的空间

  1. 输入输出流可以加一个buffer缓冲区。
  2. 如果上传的文件并不是第一时刻要用的话,合并逻辑可以异步执行,即加个Async注解。
  3. 分片大小参数化,根据生产环境服务器性能进行动态调整。

# 如果需要进行并发上传的话

可以的,并发的逻辑交给前端实现,但是后端的表结构可能需要修改,还记得我们之前有一张专门记录文件分片上传进度的数据表嘛,建表SQL如下所示:

DROP TABLE IF EXISTS `file`;
CREATE TABLE `file` (
  `id` char(8) NOT NULL DEFAULT '' COMMENT 'id',
  `path` varchar(100) NOT NULL COMMENT '相对路径',
  `name` varchar(100) DEFAULT NULL COMMENT '文件名',
  `suffix` varchar(10) DEFAULT NULL COMMENT '后缀',
  `size` int(11) DEFAULT NULL COMMENT '大小|字节B',
  `use` char(1) DEFAULT NULL COMMENT '用途|枚举[FileUseEnum]:COURSE("C", "讲师"), TEACHER("T", "课程")',
  `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime(3) DEFAULT NULL COMMENT '修改时间',
  `shard_index` int(11) DEFAULT NULL COMMENT '已上传分片',
  `shard_size` int(11) DEFAULT NULL COMMENT '分片大小|B',
  `shard_total` int(11) DEFAULT NULL COMMENT '分片总数',
  `key` varchar(32) DEFAULT NULL COMMENT '文件标识',
  PRIMARY KEY (`id`),
  UNIQUE KEY `path_unique` (`path`),
  UNIQUE KEY `key_unique` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如果需要并发shard_index就不能代表当前已上传的分片数了,取而代之的是我们必须为这个文件上传完成的每一个分片进行日志记录。所以我们可以建立下面这样一张表。

可以看到我们用file表的id和这张表进行关联,每一个分片上传完成后就将分片索引号插入到这张表中。

DROP TABLE IF EXISTS `file_shard`;
CREATE TABLE `file_shard` (
  `id` char(8) NOT NULL DEFAULT '' COMMENT 'id',
  `shard_index` int(11) DEFAULT NULL COMMENT '分片索引'
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件分片';
1
2
3
4
5
6

同步的我们合并的逻辑也得修改。

//从file_shard获取已上传完成的分片数
List<Integer> indexList=fileShradServive.selectById(fileDto.getId())

 //如果分片表的数据数和file表的total一样则可以合并
        if (indexList.size().equals(fileDto.getShardTotal())) {
            fileDto.setPath(fileFullPath);
            //文件合并
            merge(fileDto);
        }
1
2
3
4
5
6
7
8
9

断点续传同理,需要遍历file_shard表的分片索引,for循环看看缺哪个分片就把哪个分片返回给前端

List<Integer> indexList=fileShradServive.selectById(fileDto.getId())

//记录未上传的分片

List<Integer> unfinishUploadShardList=new ArrayList<>();
//分片索引排序
 Collections.sort(indexList);

        //记录未上传的分片
        List<Integer> unfinishUploadShardList = new ArrayList<>();

        for (int i = 0; i < indexList.size(); i++) {
            if (!indexList.contains(i)) {
                unfinishUploadShardList.add(i);
            }
        }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 参考文献

MultipartFile实现文件上传与下载 (opens new window)

一篇文章彻底弄懂Base64编码原理 (opens new window)

一文读懂 AWS S3 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
来聊聊Java项目分层规范
基于时间缓存优化浏览器轮询阻塞问题

← 来聊聊Java项目分层规范 基于时间缓存优化浏览器轮询阻塞问题→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×