OSS文件上传(三):断点续传
在大文件上传场景中,网络中断或客户端闪退常导致上传失败。传统方案需要重新上传整个文件,而断点续传技术允许从中断处继续上传,大幅提升用户体验并节省资源。OSS原生支持断点续传,但有时我们需要更灵活的自定义实现。本文将对比分析OSS原生方案与Redis自定义方案的差异,并详细讲解Redis实现方案。
初始化配置
(1)依赖配置(Maven)
首先在pom.xml
中添加 OSS SDK 相关依赖:
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.17.4</version>
</dependency>
如果使用的是Java 9及以上的版本,则需要添加以下JAXB相关依赖:
<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version>
</dependency>
<dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.3</version>
</dependency>
(2)OSS 配置
在application.yml
中配置 OSS 连接信息:
aliyun:oss:endpoint: oss-cn-beijing.aliyuncs.com # 地域Endpointaccess-key-id: your-access-key-idaccess-key-secret: your-access-key-secretbucket-name: your-bucket-name # 存储桶名称
创建配置类初始化 OSS 客户端:
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "aliyun")
public class AliYunConfig {private String accessKey; // 阿里云AccessKeyIdprivate String accessKeySecret; // 阿里云AccessKeySecretprivate String ossBucket; // 存储桶名称private String ossEndpoint; // OSS地域节点@Beanpublic OSS oSSClient() {return new OSSClient(ossEndpoint, accessKey, accessKeySecret);}
}
创建统一结果返回类:
package com.netflow.utils;import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;/*** 统一结果返回类* @param <T> 返回数据的类型*/public class Msg<T> implements Serializable {private static final long serialVersionUID = 1L;// 状态码private int code;// 消息private String message;// 返回数据private T data;// 时间戳private long timestamp;// 附加数据(可选)private Map<String, Object> extra;// 构造方法private Msg() {this.timestamp = System.currentTimeMillis();}private Msg(int code, String message) {this();this.code = code;this.message = message;}private Msg(int code, String message, T data) {this(code, message);this.data = data;}// 成功静态工厂方法public static <T> Msg<T> success() {return new Msg<>(200, "操作成功");}public static <T> Msg<T> success(String message) {return new Msg<>(200, message);}public static <T> Msg<T> success(T data) {return new Msg<>(200, "操作成功", data);}public static <T> Msg<T> success(String message, T data) {return new Msg<>(200, message, data);}// 失败静态工厂方法public static <T> Msg<T> fail() {return new Msg<>(500, "操作失败");}public static <T> Msg<T> fail(String message) {return new Msg<>(500, message);}public static <T> Msg<T> fail(int code, String message) {return new Msg<>(code, message);}// 链式调用方法public Msg<T> code(int code) {this.code = code;return this;}public Msg<T> message(String message) {this.message = message;return this;}public Msg<T> data(T data) {this.data = data;return this;}public Msg<T> extra(String key, Object value) {if (this.extra == null) {this.extra = new HashMap<>();}this.extra.put(key, value);return this;}// Getter方法public int getCode() {return code;}public String getMessage() {return message;}public T getData() {return data;}public long getTimestamp() {return timestamp;}public Map<String, Object> getExtra() {return extra;}@Overridepublic String toString() {return "Msg{" +"code=" + code +", message='" + message + '\'' +", data=" + data +", timestamp=" + timestamp +", extra=" + extra +'}';}
}
一、内置断点续传(setEnableCheckpoint(true)
)
核心流程
OSS通过分片上传(Multipart Upload)实现断点续传:
初始化:创建分片上传任务,获取唯一UploadID
分片上传:将文件切分为多个分片(每片5-10MB),并行上传
记录状态:OSS服务端自动记录已上传的分片信息
中断恢复:重新上传时查询UploadID对应的上传状态,仅上传缺失分片
完成合并:所有分片上传完成后,调用Complete接口合并文件
基本实现:
@PostMapping("/upload3")public Msg<String> upload(@RequestParam("file") MultipartFile file) {try {String objectName = util(file);ObjectMetadata meta = new ObjectMetadata();// 指定上传的内容类型。// meta.setContentType("text/plain");// 文件上传时设置访问权限ACL。// meta.setObjectAcl(CannedAccessControlList.Private);// 通过UploadFileRequest设置多个参数。// 依次填写Bucket名称(例如examplebucket)以及Object完整路径(例如exampledir/exampleobject.txt),Object完整路径中不能包含Bucket名称。UploadFileRequest uploadFileRequest = new UploadFileRequest(aliYunConfig.getOssBucket(),objectName);// 通过UploadFileRequest设置单个参数。// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。uploadFileRequest.setUploadFile("E:\\SpringBoot\\test\\examplefile.txt");// 指定上传并发线程数,默认值为1。uploadFileRequest.setTaskNum(5);// 指定上传的分片大小,单位为字节,取值范围为100 KB~5 GB。默认值为100 KB。uploadFileRequest.setPartSize(1 * 1024 * 1024);// 开启断点续传,默认关闭。uploadFileRequest.setEnableCheckpoint(true);// 记录本地分片上传结果的文件。上传过程中的进度信息会保存在该文件中,如果某一分片上传失败,再次上传时会根据文件中记录的点继续上传。上传完成后,该文件会被删除。// 如果未设置该值,默认与待上传的本地文件同路径,名称为${uploadFile}.ucp。//uploadFileRequest.setCheckpointFile("yourCheckpointFile");// 文件的元数据。uploadFileRequest.setObjectMetadata(meta);// 设置上传回调,参数为Callback类型。//uploadFileRequest.setCallback("yourCallbackEvent");// 断点续传上传。ossClient.uploadFile(uploadFileRequest);return Msg.success("断点续传成功");} catch (OSSException oe) {System.out.println("Caught an OSSException, which means your request made it to OSS, "+ "but was rejected with an error response for some reason.");System.out.println("Error Message:" + oe.getErrorMessage());System.out.println("Error Code:" + oe.getErrorCode());System.out.println("Request ID:" + oe.getRequestId());System.out.println("Host ID:" + oe.getHostId());} catch (Throwable ce) {System.out.println("Caught an ClientException, which means the client encountered "+ "a serious internal problem while trying to communicate with OSS, "+ "such as not being able to access the network.");System.out.println("Error Message:" + ce.getMessage());} finally {// 关闭OSSClient。if (ossClient != null) {ossClient.shutdown();}}return Msg.success("断点续传失败");}/*** UUID做前缀加工文件名* @param file* @return*/private String util(MultipartFile file){//获取初始文件名String fileName = file.getOriginalFilename();//获取文件后缀String suffixName = fileName.substring(fileName.lastIndexOf("."));//UUID作为文件名String newFileName = UUID.randomUUID().toString()+ "-"+ fileName + suffixName;return newFileName;}
原理:
- 状态存储:上传进度保存在客户端本地文件系统(如临时文件)。
- 实现方式:SDK 自动管理,无需额外组件。
- 适用场景:单机环境或无需跨设备 / 进程共享断点信息的场景。
优点:
- 简单易用:只需一行代码开启,无需配置外部存储。
- 轻量级:无需维护 Redis 等外部服务。
缺点:
- 局限性:
- 仅支持单个客户端实例(换设备 / 重启应用后断点丢失)。
- 无法在分布式系统中共享断点状态。
- 可靠性风险:本地文件可能损坏或丢失。
二、Redis自定义断点续传
当需要跨客户端恢复、自定义分片策略或对接非阿里云存储时,Redis方案更灵活:
核心流程:

基本实现:
实现原理:
- 状态存储:上传进度保存在Redis 服务器(内存或持久化存储)。
- 实现方式:开发者自定义逻辑,将断点信息序列化后存入 Redis(如使用 Hash 结构存储文件 ID、已上传块号等)。
- 适用场景:分布式系统、多设备协同或需要长期保留断点的场景。
优点:
- 跨设备 / 进程共享:支持多客户端协作上传同一文件(如浏览器和移动端同时上传)。
- 高可用性:Redis 支持集群和持久化,减少断点丢失风险。
- 扩展性:可结合 Redis 的过期策略自动清理长期未活跃的断点。
缺点:
- 复杂度高:需要额外维护 Redis 服务,并编写序列化 / 反序列化逻辑。
- 性能依赖:网络延迟可能影响断点状态的读写速度。
三、对比选择
核心差异对比表
特性 | 内置断点续传 (setEnableCheckpoint ) | Redis 断点续传 |
---|---|---|
状态存储位置 | 客户端本地文件系统 | Redis 服务器(内存 / 磁盘) |
跨设备支持 | ❌ 仅支持单设备 | ✅ 多设备 / 进程共享 |
实现复杂度 | ✅ 简单(SDK 自动管理) | ❌ 需要自定义代码和 Redis 维护 |
断点持久化 | ❌ 客户端重启可能丢失 | ✅ Redis 可持久化(如 RDB/AOF) |
分布式系统适用 | ❌ 不适用 | ✅ 天然支持 |
典型场景 | 单机上传、临时文件上传 | 企业级应用、多端协同上传 |
Redis方案优化建议
状态过期策略:设置Redis Key过期时间(如TTL=7天)
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
分片数据存储:
方案1:直接存储Base64(适合小文件)
方案2:将分片暂存OSS,Redis仅记录ETag(推荐大文件)
并发控制:
// 使用Redis分布式锁保证分片状态更新原子性
RLock lock = redissonClient.getLock("LOCK:" + fileMd5);
lock.lock();
try {// 更新上传状态
} finally {lock.unlock();
}
何时选择哪种方式?
选内置断点续传:
- 简单应用,无需跨设备 / 进程断点共享。
- 临时文件上传,断点丢失影响较小。
- 快速实现,不想引入外部依赖。
选 Redis 断点续传:
- 分布式系统或微服务架构。
- 需要多用户 / 设备协作上传同一文件。
- 断点信息需要长期保留或审计。
- 已有 Redis 基础设施,希望复用资源。
总结:
内置断点续传是快速实现单设备断点功能的首选,而 Redis 方案则提供了更强大的分布式和持久化能力,适合复杂场景。开发者应根据实际场景选择:优先使用原生能力降低复杂度,在需要深度定制时采用Redis方案。两种方案的核心思想都是"分片上传+状态记录",理解这一原理能帮助我们在任何存储系统中实现可靠的文件传输。
OSS文件上传----完结!!!