---
title: "06-图片优化"
created: 2025-12-02
tags:
- 项目
aliases:
- 图片优化
---
# 图片优化
## 本节重点
之前虽然我们已经完成了云图库项目的功能开发,但在上线之前,还有很大的优化空间。
本节教程中,鱼皮会分享近 10 种主流的图片优化技术,包括:
- 图片查询优化 - 分布式缓存、本地缓存、多级缓存
- 图片上传优化 - 压缩、秒传、分片上传、断点续传
- 图片加载优化 - 懒加载、缩略图、CDN 加速、浏览器缓存
- 图片存储优化 - 降频存储(冷热数据分离)、清理策略
💡 鱼皮的 [代码生成器共享平台项目](https://www.codefather.cn/course/1790980795074654209) 中,也详细讲解过存储优化策略,感兴趣的同学可以去学习下。
## 一、图片查询优化
### 缓存
对于经常访问的数据,每次都从数据库(硬盘)中获取是比较慢,可以利用性能更高的存储来提高系统响应速度,俗称缓存。
合理使用缓存可以显著降低数据库的压力、提高系统性能。
那么,什么样的数据适合缓存呢?一般情况下就 4 个字 **“读多写少”**,要频繁查询的、不怎么修改的。
具体来说:
1. 高频访问的数据:如系统首页、热门推荐内容等。
2. 计算成本较高的数据:如复杂查询结果、大量数据的统计结果。
3. 允许短时间延迟的数据:如不需要实时更新的排行榜、图片列表等。
在我们的项目中,主页是用户高频访问的内容,调用的获取图片列表的接口也是高频访问的。而且即使数据更新存在一定延迟,也不会对用户体验造成明显影响,因此非常适合缓存。
### Redis 分布式缓存
分布式缓存是指将缓存数据分布存储在 **多台服务器** 上,以便在高并发场景下提供更高的吞吐量和更好的容错性。
Redis 是实现分布式缓存的主流方案,也是后端开发必学的技能。主要是由于它具有下面几个优势:
- 高性能:基于内存操作,访问速度极快。**单节点 Redis 的读写 QPS 可达 10w 次每秒!**
- 丰富的数据结构:支持字符串、列表、集合、哈希、位图等,适用于各种数据结构存储。
- 分布式支持:可以通过 Redis Cluster 构建高可用、高性能的分布式缓存,还提供哨兵集群机制提升可用性、提供分片集群机制提高可扩展性。
#### 缓存设计
需要缓存首页的图片列表数据,也就是对 listPictureVOByPage 接口进行缓存。首先按照缓存 3 要素 “key、value、过期时间”
进行设计。
1)缓存 key 设计
由于接口支持传入不同的查询条件,对应的数据不同,因此需要将查询条件作为缓存 key 的一部分。
可以将查询条件对象转换为 JSON 字符串,但这个 JSON 会比较长,可以利用哈希算法(md5)来压缩 key。
此外,由于使用分布式缓存,可能由多个项目和业务共享,因此需要在 key 的开头拼接前缀进行隔离。设计出的 key 如下:
```java
yupicture:listPictureVOByPage:${查询条件key}
```
2)缓存 value 设计
缓存从数据库中查到的 Page 分页对象,存储为什么格式呢?这里有 2 种选择:
- 为了可读性,可以转换为 JSON 结构的字符串
- 为了压缩空间,可以存为二进制等其他结构
但是对应的 Redis 数据结构都是 string。
3)缓存过期时间设置
**必须设置缓存过期时间!**根据实际业务场景和缓存空间的大小、数据的一致性的要求设置,合适即可,此处由于查询条件较多、而且考虑到图片会持续更新,设置为
5 ~ 60 分钟即可。
#### 如何操作 Redis?
Java 中有非常多的 Redis 操作库,比如 Jedis、Lettuce 等。为了便于和 Spring 项目集成,Spring 还提供了
Spring Data Redis 作为操作 Redis 的更高层抽象(默认使用 Lettuce 作为底层客户端)。由于我们的项目使用 Spring
Boot,也推荐使用 [Spring Data Redis](https://spring.io/projects/spring-data-redis/),开发成本更低。
它的使用也非常简单,我们直接上手项目实战。
#### 后端开发
1)引入 Maven 依赖,使用 Spring Boot Starter 快速整合 Redis:
```xml
org.springframework.boot
spring-boot-starter-data-redis
```
2)在 application.yml 中添加 Redis 配置:
```yaml
# redis 配置
redis:
host: localhost # Redis 主机
port: 6379 # Redis 端口
database: 2 # Redis 使用的数据库索引
# session 配置
session:
store-type: redis # 将 session 存储在 Redis 中
redis:
namespace: session # 设置 Redis 中存储 session 数据的命名空间
flush-mode: on_save # 表示当 session 更新时,才会将数据保存到 Redis
timeout: 86400 # session 过期时间:24小时
# 在RedisTemplateConfig中 使用@EnableRedisHttpSession注解
# 表示用自定义的 RedisTemplate 来存储 session 数据
# 确保 session 数据以 JSON 格式存储在 Redis 中 而不是默认的 JDK 序列化 显示为乱码
```
默认情况下,Spring Boot的RedisTemplate使用JdkSerializationRedisSerializer作为键/值的序列化器,这会导致:
键名会被序列化为二进制格式
QuickRedis等客户端无法直接识别二进制键名
即使数据存储成功,但通过客户端查看时显示为乱码或无法识别
通过自定义RedisTemplate配置,将键和值的序列化方式改为String格式:
3)编写 JUnit 单元测试文件,测试使用 `StringRedisTemplate` 完成对 Redis 的基础操作(增删改查):
```java
@SpringBootTest
public class RedisStringTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testRedisStringOperations() {
// 获取操作对象
ValueOperations valueOps = stringRedisTemplate.opsForValue();
// Key 和 Value
String key = "testKey";
String value = "testValue";
// 1. 测试新增或更新操作
valueOps.set(key, value);
String storedValue = valueOps.get(key);
assertEquals(value, storedValue, "存储的值与预期不一致");
// 2. 测试修改操作
String updatedValue = "updatedValue";
valueOps.set(key, updatedValue);
storedValue = valueOps.get(key);
assertEquals(updatedValue, storedValue, "更新后的值与预期不一致");
// 3. 测试查询操作
storedValue = valueOps.get(key);
assertNotNull(storedValue, "查询的值为空");
assertEquals(updatedValue, storedValue, "查询的值与预期不一致");
// 4. 测试删除操作
stringRedisTemplate.delete(key);
storedValue = valueOps.get(key);
assertNull(storedValue, "删除后的值不为空");
}
}
```
4)新写一个使用缓存的分页查询图片列表的接口。在查询数据库前先查询缓存,如果已有数据则直接返回缓存,如果没有数据则查询数据库,并且将结果设置到缓存中。
代码如下:
```java
@PostMapping("/list/page/vo/cache")
public BaseResponse> listPictureVOByPageWithCache(@RequestBody PictureQueryRequest pictureQueryRequest,
HttpServletRequest request) {
long current = pictureQueryRequest.getCurrent();
long size = pictureQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
// 普通用户默认只能查看已过审的数据
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
// 构建缓存 key
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
String redisKey = "yupicture:listPictureVOByPage:" + hashKey;
// 从 Redis 缓存中查询
ValueOperations valueOps = stringRedisTemplate.opsForValue();
String cachedValue = valueOps.get(redisKey);
if (cachedValue != null) {
// 如果缓存命中,返回结果
Page cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
// 查询数据库
Page picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
// 获取封装类
Page pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
// 存入 Redis 缓存
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
// 5 - 10 分钟随机过期,防止雪崩
int cacheExpireTime = 300 + RandomUtil.randomInt(0, 300);
valueOps.set(redisKey, cacheValue, cacheExpireTime, TimeUnit.SECONDS);
// 返回结果
return ResultUtils.success(pictureVOPage);
}
```
#### 测试
可以通过 Swagger 测试一下返回结果是否正常,并且对比和之前查数据库的性能提升。
没缓存,平均 50ms:
![[eBmAsIFzG3OHQSF6-4b5d17cd.webp]]
有缓存,平均 20~30 ms,性能显著提升!
![[ABaTXddNrNGK51Hs-bcaa8cb2.webp]]
细心的同学会发现,为什么接口返回的大小不一样呢?因为缓存的过程中我们将 JSON 字符串和 Java 对象进行了转换,使得一些为 null
的字段被过滤掉了。
### Caffeine 本地缓存
当应用需要频繁访问某些数据时,可以将这些数据缓存到应用的内存中(比如 JVM 中);下次访问时,直接从内存读取,而不需要经过网络或其他存储系统。
相比于分布式缓存,本地缓存的速度更快,但是无法在多个服务器间共享数据、而且不方便扩容。
所以本地缓存的应用场景一般是:
- 数据访问量有限的小型数据集
- 不需要服务器间共享数据的单机应用
- 高频、低延迟的访问场景(如用户临时会话信息、短期热点数据)。
对于 Java 项目,[Caffeine](https://github.com/ben-manes/caffeine) 是主流的本地缓存技术,拥有极高的性能和丰富的功能。比如可以精确控制缓存数量和大小、支持缓存过期、支持多种缓存淘汰策略、支持异步操作、线程安全等。
💡 鱼皮建议,由于本地缓存不需要引入额外的中间件,成本更低。因此如果只是要提升数据访问性能,优先考虑本地缓存而不是分布式缓存。
#### 缓存设计
本地缓存的设计和分布式缓存基本一致,不再赘述。但有 2 个区别:
1. 本地缓存需要自己创建初始化缓存结构(可以简单理解为要自己 new 一个 HashMap)。
2. 由于本地缓存本身就是服务器隔离的,而且占用服务器的内存,key 可以更精简一些,不用再添加项目前缀。
#### 后端开发
1)[参考官方文档](https://github.com/ben-manes/caffeine) 引入 Caffeine
的 Maven 依赖,注意如果要引入 3.x 版本的 Caffeine,Java 版本必须 >= 11!如果不想升级 JDK,也可以改为引入
2.x 版本。
```xml
com.github.ben-manes.caffeine
caffeine
3.1.8
```
2)构造本地缓存,设置缓存容量和过期时间:
```java
private final Cache LOCAL_CACHE =
Caffeine.newBuilder().initialCapacity(1024)
.maximumSize(10000L)
// 缓存 5 分钟移除
.expireAfterWrite(5L, TimeUnit.MINUTES)
.build();
```
3)参考之前使用分布式缓存的代码,修改为使用本地缓存。在查询数据库前先查询本地缓存,如果已有数据则直接返回缓存:
```java
// 构建缓存 key
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
String cacheKey = "listPictureVOByPage:" + hashKey;
// 从本地缓存中查询
String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey);
if (cachedValue != null) {
// 如果缓存命中,返回结果
Page cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
```
如果没有数据则查询数据库,并且将结果设置到本地缓存中:
```java
// 查询数据库
Page picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
// 获取封装类
Page pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
// 存入本地缓存
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
LOCAL_CACHE.put(cacheKey, cacheValue);
```
4)可以通过 Swagger 测试一下返回结果是否正常,并且对比和之前查数据库、查 Redis 的性能提升。
有缓存,最快可达 12ms,性能又进一步提升了 1 倍左右,相比数据库提升了好几倍!
![[oNsfapMBymRgsnE4-94ca4244.webp]]
而且目前我们数据库和 Redis 都是在本地的,本来访问就比较快,如果使用远程数据库或 Redis,性能的提升会更为明显。
#### 扩展
我们发现,使用本地缓存和分布式缓存的流程基本是一致的。那么思考一下,如果你想灵活地切换使用本地缓存或分布式缓存,应该怎么实现呢?
答案:策略模式或者模板方法模式。
### 多级缓存
多级缓存是指结合本地缓存和分布式缓存的优点,在同一业务场景下构建两级缓存系统,这样可以兼顾本地缓存的高性能、以及分布式缓存的数据一致性和可靠性。
多级缓存的工作流程:
1. 第一级(Caffeine 本地缓存):优先从本地缓存中读取数据。如果命中,则直接返回。
2. 第二级(Redis 分布式缓存):如果本地缓存未命中,则查询 Redis 分布式缓存。如果 Redis 命中,则返回数据并更新本地缓存。
3. 数据库查询:如果 Redis 也未命中,则查询数据库,并将结果写入 Redis 和本地缓存。
流程图:
![[3WLaI8VdRHNyoTW7-6436b8df.webp]]
多级缓存还有一个优势,就是提升了系统的容错性。即使 Redis 出现故障,本地缓存仍可提供服务,减少对数据库的直接依赖。
#### 后端开发
1)优先从本地缓存中读取数据。如果命中,则直接返回。
```java
// 构建缓存 key
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
String cacheKey = "yupicture:listPictureVOByPage:" + hashKey;
// 1. 查询本地缓存(Caffeine)
String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey);
if (cachedValue != null) {
Page cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
```
2)如果本地缓存未命中,则查询 Redis 分布式缓存。如果 Redis 命中,则返回数据并更新本地缓存。
```java
// 2. 查询分布式缓存(Redis)
ValueOperations valueOps = stringRedisTemplate.opsForValue();
cachedValue = valueOps.get(cacheKey);
if (cachedValue != null) {
// 如果命中 Redis,存入本地缓存并返回
LOCAL_CACHE.put(cacheKey, cachedValue);
Page cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
```
3)如果 Redis 也未命中,则查询数据库,并将结果写入 Redis 和本地缓存。
```java
// 3. 查询数据库
Page picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
Page pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
// 4. 更新缓存
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
// 更新本地缓存
LOCAL_CACHE.put(cacheKey, cacheValue);
// 更新 Redis 缓存,设置过期时间为 5 分钟
valueOps.set(cacheKey, cacheValue, 5, TimeUnit.MINUTES);
```
### 扩展
#### 1、手动刷新缓存
在某些情况下,数据更新较为频繁,但自动刷新缓存机制可能存在延迟,可以通过手动刷新来解决。
比如:
- 提供一个刷新缓存的接口,仅管理员可调用。
- 提供管理后台,支持管理员手动刷新指定缓存。
#### 2、解决缓存常见问题
使用缓存时,一般要注意下面几个问题:
1)缓存击穿:某些 **热点数据** 在缓存过期后,大量请求直接打到数据库。
解决方案:设置热点数据的超长过期时间,或使用互斥锁(如 Redisson)控制缓存刷新。
2)缓存穿透:用户频繁请求不存在的数据,导致大量的请求直接触发数据库查询。
解决方案:对无效查询结果也进行缓存(如设置空值缓存),或者使用布隆过滤器。
3)缓存雪崩:大量缓存同时过期,导致请求打到数据库,系统崩溃。
解决方案:设置不同缓存的过期时间,避免同时过期;或者使用多级缓存,减少对数据库的依赖。
💡《如何解决缓存中的常见问题》是一道经典的八股文,感兴趣的同学可以 [在面试鸭上阅读学习](https://www.mianshiya.com/question/1780933295672946690)。
#### 3、自动识别热点图片缓存
可以采用热 key 探测技术,实时对图片的访问量进行统计,并自动将热点图片添加到内存缓存,以应对大量高频的访问。
[鱼皮的面试刷题平台项目](https://www.codefather.cn/course/1826803928691945473) 中对该技术有详细的讲解,有时间可以学习下。
#### 4、查询优化
可以参考 [数据库优化技巧](https://www.mianshiya.com/question/1816438385021997058),比如获取图片列表时只查询(select)必要的字段,返回给前端时也只返回必要的字段等。
#### 5、代码优化
如果操作缓存的逻辑更复杂,可以单独抽象 CacheManager 统一管理缓存相关操作。
## 二、图片上传优化
### 图片压缩
对于图库网站来说,图片压缩是图片优化中最基本且最重要的操作,能够显著减少图片文件的大小,从而降低带宽使用和流量消耗,大幅降低成本的同时,提高图片加载速度。
有哪些压缩图片的方法呢?
1. 将图片格式转换为体积更小的格式,比如 WebP 或其他现代格式
2. 对图片质量进行压缩
3. 缩小图片尺寸
当然对于图片网站来说,我们希望尽可能不要影响图片的质量,因此更推荐第 1 种方法。
那么将图片压缩成什么格式?如何对图片进行压缩呢?
#### 1、图片压缩格式
格式上,有 2 种选择:
1)WebP:由 Google 开发的现代图片格式,支持有损和无损压缩。相比传统格式:
- 比 PNG 文件小约 26%。
- 比 JPEG 文件小约 25%-34%。
- 支持透明背景(Alpha 通道)。
- 兼容性:大部分主流浏览器(如 Chrome、Edge、Firefox 等)均已支持 WebP。
2)AVIF:基于 AV1 视频编码技术的图片格式,压缩率更高。
- 比 WebP 的文件大小更小,画质更优。
- 支持透明背景和高动态范围(HDR)。
虽然 AVIF 看起来更牛,但目前其兼容性没有 WebP 要好,为了保证图片在不同浏览器都能正常加载,**建议选择 WebP 格式**。
#### 2、图片压缩方案
跟解析图片的操作一样,可以使用本地的图像处理类库自行操作,也可以利用第三方云服务完成。
因为我们图片已经上传到了腾讯云 COS 对象存储服务,可以直接利用数据万象服务。通过配置图片处理规则,在图片上传的同时自动进行压缩处理,减少开发成本。
如何利用数据万象对图片进行压缩呢?官方提供了 [最佳实践文档](https://cloud.tencent.com/document/product/460/72229) ,主要有
2 种压缩方式:
1. 访问图片时实时压缩
2. 上传图片时实时压缩,[参考文档](https://cloud.tencent.com/document/product/436/54050#.E4.B8.8A.E4.BC.A0.E6.97.B6.E5.A4.84.E7.90.86)
其实还有第三种方式,也可以对已上传的图片进行压缩处理,[参考文档](https://cloud.tencent.com/document/product/436/54050#.E4.BA.91.E4.B8.8A.E6.95.B0.E6.8D.AE.E5.A4.84.E7.90.86)。
对于我们的需求,要将图片格式转化为 WebP,可以 [参考官方文档](https://cloud.tencent.com/document/product/436/44883),在上传文件时,传入
Rules 规则。使用 HTTP API 调用时,传入处理规则参数:
![[cSt4qMZgpWH3B7O1-58652d5a.webp]]
如果使用 SDK,就需要构造图片处理规则对象,[参考文档](https://cloud.tencent.com/document/product/436/55377#.E4.B8.8A.E4.BC.A0.E6.97.B6.E5.9B.BE.E7.89.87.E6.8C.81.E4.B9.85.E5.8C.96.E5.A4.84.E7.90.86):
![[DuLnjQeduxZofIuS-be40ae1e.webp]]
#### 3、后端开发
为了实现方便,我们此处仅对文件格式进行转化,不进行质量变换之类的其他处理。
1)修改 CosManager 上传图片的方法,将图片后缀转为 webp,并且使用数据万象将图片格式转为 webp:
```java
/**
* 上传对象(附带图片信息)
*
* @param key 唯一键
* @param file 文件
*/
public PutObjectResult putPictureObject(String key, File file) {
try {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file);
// 对图片进行处理(获取基本信息也被视作为一种处理)
PicOperations picOperations = new PicOperations();
// 1 表示返回原图信息
picOperations.setIsPicInfo(1);
//-------压缩为.webp上传-------- 仅实现于临时文件上传模块(学习使用) 因为是图片服务 最好还是保留高品质图片在cos上
List rules = new ArrayList<>();
String webpKey = FileUtil.mainName(key) + ".webp";
PicOperations.Rule compressRule = new PicOperations.Rule();
compressRule.setRule("imageMogr2/format/webp");
compressRule.setBucket(cosClientConfig.getBucket());
compressRule.setFileId(webpKey);
rules.add(compressRule);
picOperations.setRules(rules);
//-------------end-------------
// 构造处理参数
putObjectRequest.setPicOperations(picOperations);
return cosClient.putObject(putObjectRequest);
} catch (Exception e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "图片对象上传失败: " + e.getMessage());
}
}
```
2)修改 PictureUploadTemplate 上传图片的方法,从图片处理结果中获取到缩略图,并设置到返回结果中:
```java
try {
file = File.createTempFile(uploadFilename, null);
processFile(inputSource, file);
PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);
ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();
//-------压缩为.webp上传-------- 仅实现于临时文件上传模块(学习使用) 因为是图片服务 最好还是保留高品质图片在cos上
ProcessResults processResults = putObjectResult.getCiUploadResult().getProcessResults();
List objectList = processResults.getObjectList();
if (CollUtil.isNotEmpty(objectList)) {
CIObject compressedCiObject = objectList.get(0);
// 封装压缩图返回结果
return buildResult(originFilename, compressedCiObject);
}
//-------------end-------------
// 封装原图返回结果
return buildResult(originFilename, file, uploadPath, imageInfo);
} catch (IOException e) {
```
3)编写新的封装返回结果方法,从压缩图中获取图片信息:
```java
private UploadPictureResult buildResult(String originFilename, CIObject compressedCiObject) {
UploadPictureResult uploadPictureResult = new UploadPictureResult();
int picWidth = compressedCiObject.getWidth();
int picHeight = compressedCiObject.getHeight();
double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();
uploadPictureResult.setPicName(FileUtil.mainName(originFilename));
uploadPictureResult.setPicWidth(picWidth);
uploadPictureResult.setPicHeight(picHeight);
uploadPictureResult.setPicScale(picScale);
uploadPictureResult.setPicFormat(compressedCiObject.getFormat());
uploadPictureResult.setPicSize(compressedCiObject.getSize().longValue());
uploadPictureResult.setUrl(compressedCiObject.getKey());// 设置图片为压缩后的地址
return uploadPictureResult;
}
```
#### 4、测试
上传图片并查看对象存储中的资源大小,发现压缩效果显著。而且压缩图和原图同名,便于查找原图:
![[O88LmOWLaEKTHPib-18ff7c22.webp]]
前端也能正常获取到压缩后的图片信息:
![[iHeRrdiMyNmmRGwi-aacb2864.webp]]
#### 扩展
1)增加对原图的处理
目前每次上传图片实际上会保存原图和压缩图 2 个图片,原图占用的空间还是比较大的。如果想进一步优化,可以删除原图,只保留缩略图;或者在数据库中保存原图的地址,用作备份。
2)尝试更大比例的压缩,比如使用 [质量变换](https://cloud.tencent.com/document/product/436/44884) 来处理图片。
### 扩展知识 - 文件秒传
由于文件秒传对于图片上传场景的作用有限,仅作为扩展知识学习即可,不必在本项目中实现。
文件秒传是一种基于文件的唯一标识(如 MD5、SHA-256)对文件内容进行快速校验,避免重复上传的方法,在大型文件传输场景下非常重要。可以提高性能、节约带宽和存储资源。
大家可能用过 XX 网盘软件,如果重复上传相同的文件 2 次,你会发现第二次的上传速度贼快!
#### **文件秒传的实现方案**
文件秒传的实现并不复杂,流程如下:
1)客户端生成文件唯一标识:上传前,通过客户端计算文件的哈希值(如 MD5、SHA-256),生成文件的唯一指纹。
2)服务端校验文件指纹:后端接收到文件指纹后,在存储中查询是否已存在相同文件。
- 若存在相同文件,则直接返回文件的存储路径。
- 若不存在相同文件,则接收并存储新文件,同时记录其指纹信息。
注意,客户端和服务端是相对的概念。因为现在我们要把文件上传到对象存储服务器,我们的后端此时就是“客户端”,对象存储服务器才是 “服务端”。
对于我们的项目,给图片表增加 md5 字段用于存储文件指纹,上传图片前增加类似的逻辑判断即可:
```java
// 计算文件指纹
String md5 = SecureUtil.md5(file);
// 从数据库中查询已有的文件
List pictureList = pictureService.lambdaQuery()
.eq(Picture::getMd5, md5)
.list();
// 文件已存在,秒传
if (CollUtil.isNotEmpty(pictureList)) {
// 直接复用已有文件的信息,不必重复上传文件
Picture existPicture = pictureList.get(0);
} else {
// 文件不存在,实际上传逻辑
}
```
#### **实际使用中的限制**
我们目前的项目其实不适合使用文件秒传。一方面是对于图片场景,文件比较小、重复文件也相对较少,秒传的优化效果有限;另外一方面是本项目使用腾讯云
COS 的对象存储,只能通过唯一地址去取文件,无法完全自定义文件的存储结构、也不支持文件快捷方式的概念,因此秒传的文件地址必须使用和原文件相同的对象路径,可能导致其他的问题(比如用户
A 上传的图片地址等同于用户 B 上传的地址)。
### 扩展知识 - 分片上传和断点续传
对于大文件,还可以开启分片上传和断点续传,不需要自己开发,直接使用 [对象存储的 SDK](https://cloud.tencent.com/document/product/436/65935) 就能完成。
![[ppE70rHmeBpnjbDY-98acb6e7.webp]]
## 三、图片加载优化
图片加载优化的目的是提升页面加载速度、减少带宽消耗,并改善用户体验。
本节鱼皮将从缩略图、懒加载、CDN 加速、浏览器缓存这 4 个方面进行全面优化。
### 缩略图
系统目前的问题:首页直接加载原图,原图文件通常比缩略图大数倍甚至数十倍,不仅导致加载时间长,还会造成大量流量浪费。
解决方案:上传图片时,同时生成一份较小尺寸的缩略图。用户浏览图片列表时加载缩略图,只有在进入详情页或下载时才加载原图。
#### 权衡
当前我的图片服务已经引入了webp压缩格式 在主页拉取时 使用webp展示 进入详情页才显示原本的jpg或png图片 是否还需要引入cos的缩略图
让主页显示缩略图?缩略图对比webp会减轻很多吗 质量下降又如何 带来的正负面影响权衡一下
1. 缩略图与WebP的互补性
WebP的优势:通过格式转换降低单张图片文件体积(通常比JPEG小30-50%),但无法改变图片物理尺寸。
缩略图的优势:通过缩小尺寸进一步压缩体积(如原图1080p→200x200缩略图,体积可能减少90%以上),对加载速度影响更直接。
结合效果:WebP+缩略图的组合比单独使用任一方案能减少 80%+ 的传输流量,是性能优化的黄金组合。
2. 正负面影响权衡
![[image-4f569f6a.png]]
3. 实施建议
优先方案:使用COS的动态缩略图功能(如通过URL参数指定尺寸),无需预先存储,按需生成。例如:
```text
https://cos.bucket.com/{original.jpg}?imageM=resize,str_200_200&fmt=webp
```
配置要点:
设置缩略图尺寸(如200x200像素适配列表页)
强制转为WebP格式(fmt=webp)
开启质量参数(如q\_70平衡质量与体积)
质量控制:通过COS的q\_参数控制压缩质量(如q\_75),在列表页可接受70-80%的WebP质量,细节损失用户不易察觉。
4. 技术收益量化参考
![[image-3aa8b4bc.png]]
5. 风险规避措施
回退机制:设置缩略图生成失败时回退到原图WebP版本。
版本控制:通过URL参数区分不同尺寸版本,避免缓存冲突。
A/B测试:对比不同质量参数对用户体验和性能的影响,选择最优平衡点。
结论:强烈建议引入COS缩略图功能,结合WebP可实现显著性能提升,且通过动态生成方案可规避存储成本问题。
动态缩略图服务是需要数据万象额外计费的
假设主页列表页展示100张图片,每张缩略图请求参数为 imageM=resize,str\_200\_200&fmt=webp:
单次请求成本:
图像处理请求:100次/10,000 = 0.01 次计费单位 → 约0.00005元
处理后流量:假设每张缩略图20KB → 总流量 100×20KB = 2MB ≈ 0.0007元
合计约0.00075元/次请求。
虽然后续可以用cdn再降低成本
但还是没必要 自己手动压缩一份就好了 用存储空间和开发时间换钱(空间:合着就我最不值钱)
#### 1、实现方案
生成缩略图的方法和前面讲的 “图片压缩” 一致,可以使用本地图像处理类库,也可以利用第三方云服务完成。
此处我们依然选择数据万象服务,[参考 Java SDK 文档](https://cloud.tencent.com/document/product/436/55377#.E4.B8.8A.E4.BC.A0.E6.97.B6.E5.9B.BE.E7.89.87.E6.8C.81.E4.B9.85.E5.8C.96.E5.A4.84.E7.90.86) 使用
SDK 来构造图片处理规则对象,具体的图片缩放参数可 [参考文档](https://cloud.tencent.com/document/product/436/44880):
![[Wa3mO0pH4xLB52WQ-a071c756.webp]]
#### 2、后端开发
1)数据表 picture 新增缩略图字段:
```sql
ALTER TABLE picture
-- 添加新列
ADD COLUMN thumbnailUrl varchar(512) NULL COMMENT '缩略图 url';
```
2)PictureMapper.xml 新增缩略图字段:
```xml
id,url,thumbnailUrl,name,
introduction,category,tags,
picSize,picWidth,picHeight,
picScale,picFormat,userId,
createTime,editTime,updateTime,
isDelete
```
3)数据模型新增缩略图字段,包括 Picture 类、PictureVO 类、UploadPictureResult 类:
```java
/**
* 缩略图 url
*/
private String thumbnailUrl;
```
4)缩略图处理
首先明确我们使用的缩放规则,设置最大宽高后,对图片进行等比缩小。且如果缩略图的宽高大于原图,则不会处理。
![[p2zSPB1YnAXjLzgq-ac7b1616.webp]]
修改 CosManager 的上传图片方法,补充对缩略图的处理:
```java
public PutObjectResult putPictureObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
// 对图片进行处理(获取基本信息也被视作为一种处理)
PicOperations picOperations = new PicOperations();
// 1 表示返回原图信息
picOperations.setIsPicInfo(1);
List rules = new ArrayList<>();
// 图片压缩(转成 webp 格式)
String webpKey = FileUtil.mainName(key) + ".webp";
PicOperations.Rule compressRule = new PicOperations.Rule();
compressRule.setRule("imageMogr2/format/webp");
compressRule.setBucket(cosClientConfig.getBucket());
compressRule.setFileId(webpKey);
rules.add(compressRule);
// 缩略图处理
PicOperations.Rule thumbnailRule = new PicOperations.Rule();
thumbnailRule.setBucket(cosClientConfig.getBucket());
String thumbnailKey = FileUtil.mainName(key) + "_thumbnail." + FileUtil.getSuffix(key);
thumbnailRule.setFileId(thumbnailKey);
// 缩放规则 /thumbnail/x>(如果大于原图宽高,则不处理)
thumbnailRule.setRule(String.format("imageMogr2/thumbnail/%sx%s>", 128, 128));
rules.add(thumbnailRule);
// 构造处理参数
picOperations.setRules(rules);
putObjectRequest.setPicOperations(picOperations);
return cosClient.putObject(putObjectRequest);
}
```
修改 PictureUploadTemplate 的上传图片方法,获取到缩略图:
```java
ProcessResults processResults = putObjectResult.getCiUploadResult().getProcessResults();
List objectList = processResults.getObjectList();
if (CollUtil.isNotEmpty(objectList)) {
CIObject compressedCiObject = objectList.get(0);
CIObject thumbnailCiObject = objectList.get(1);
// 封装压缩图返回结果
return buildResult(originFilename, compressedCiObject, thumbnailCiObject);
}
```
修改 PictureUploadTemplate 封装返回结果的方法,将缩略图路径也设置到返回结果中:
```java
private UploadPictureResult buildResult(String originFilename, CIObject compressedCiObject, CIObject thumbnailCiObject) {
UploadPictureResult uploadPictureResult = new UploadPictureResult();
// ...
// 设置缩略图
uploadPictureResult.setThumbnailUrl(cosClientConfig.getHost() + "/" + thumbnailCiObject.getKey());
return uploadPictureResult;
}
```
需要同步修改 PictureService 的上传图片方法,补充设置缩略图字段:
```java
// 构造要入库的图片信息
Picture picture = new Picture();
picture.setUrl(uploadPictureResult.getUrl());
picture.setThumbnailUrl(uploadPictureResult.getThumbnailUrl());
String picName = uploadPictureResult.getPicName();
```
5)测试效果
上传大图片时,缩略图的效果显著,体积直接减小百倍!
![[T2u90XsMuw66s3Qx-73ba6a14.webp]]
但有个比较坑的情况,如果上传的图片本身就比较小,缩略图反而比压缩图更大,还不如不缩略!
![[qX6kfuD8w9vcsgNI-073efd2a.webp]]
我们可以优化 CosManager 图片上传的逻辑,仅对 > 20 KB 的图片生成缩略图:
```java
// 缩略图处理,仅对 > 20 KB 的图片生成缩略图
if (file.length() > 2 * 1024) {
PicOperations.Rule thumbnailRule = new PicOperations.Rule();
thumbnailRule.setBucket(cosClientConfig.getBucket());
String thumbnailKey = FileUtil.mainName(key) + "_thumbnail." + FileUtil.getSuffix(key);
thumbnailRule.setFileId(thumbnailKey);
// 缩放规则 /thumbnail/x>(如果大于原图宽高,则不处理)
thumbnailRule.setRule(String.format("imageMogr2/thumbnail/%sx%s>", 128, 128));
rules.add(thumbnailRule);
}
```
修改 PictureUploadTemplate 的逻辑,如果没有生成缩略图,则缩略图等于压缩图:
```java
if (CollUtil.isNotEmpty(objectList)) {
CIObject compressedCiObject = objectList.get(0);
// 缩略图默认等于压缩图
CIObject thumbnailCiObject = compressedCiObject;
// 有生成缩略图,才得到缩略图
if (objectList.size() > 1) {
thumbnailCiObject = objectList.get(1);
}
// 封装压缩图返回结果
return buildResult(originFilename, compressedCiObject, thumbnailCiObject);
}
```
#### 3、前端开发
只需要将首页展示的图片改为优先读取缩略图即可:
```html
```
效果如图:
![[w5ybXF4ycUeSLUvV-491cf9f6.webp]]
如果觉得太模糊了,还可以增大缩略图的宽高、调高需要生成缩略图的文件大小阈值,这是一个成本和用户体验上的权衡~
#### 扩展
1)除了缩略图之外,还可以提供相对更清晰的 **预览图**,用于在图片详情页展示,仅在下载时才使用无损原图。
2)可以对文件名称进行更完整地校验处理,防止因为图片本身名称不规范导致的后缀丢失。
### 懒加载
懒加载(Lazy Loading)可以避免一次性加载所有图片,**只有当资源需要显示时才进行加载**。比如对于图片列表来说,仅在用户滚动到图片所在区域时才加载该图片资源。
#### 实现方案
虽然懒加载的实现更多的是依赖前端,但后端也有一定的优化策略,比如对图片列表进行分页,每页不需要展示过多的内容。
前端如何实现懒加载呢?
1)使用 HTML5 原生的 `loading="lazy"` 属性。示例代码如下:
```html
```
这种方法最简单,但是对旧版浏览器(如 IE)不兼容,而且不支持更复杂的懒加载需求(如动画或特殊事件触发)。
2)使用 JS 的 Intersection Observer,这个 API 能够检测元素是否进入视口,参考实现如下:
1. 将图片的真实 src 替换为一个占位属性(如 data-src)。
2. 使用 Intersection Observer 监听图片是否进入视口。
3. 当图片进入视口时,将 data-src 的值赋给 src,触发加载。
```java
```
3)使用 JS 监听页面滚动事件实现。每次页面滚动时,判断图片是否进入可视区域;如果是,则给图片增加 src 属性,触发图片加载。
4)使用现成的组件库或类库实现,比如 [lazysizes 库](https://github.com/aFarkas/lazysizes)。
由于我们的项目已经使用了缩略图,加载的资源本来也不多,所以暂时没必要使用懒加载了。
#### 扩展知识 - 渐进式加载
渐进式加载和懒加载技术类似,先加载低分辨率或低质量的占位资源(如模糊的图片缩略图),在用户访问或等待期间逐步加载高分辨率的完整资源,加载完成后再替换掉占位资源。
适用于超清图片加载、用户体验要求较高的页面,在网络环境较差时,效果会更明显。
Ant Design Vue 的 [Image 图片组件](https://antdv.com/components/image-cn) 支持渐进加载功能:
![[UnFTysP1LCGWQ02F-cd76ac96.webp]]
### CDN 加速
#### 1、什么是 CDN?
CDN(内容分发网络)是通过将图片文件分发到全球各地的节点,用户访问时从离自己最近的节点获取资源的技术,常用于文件资源或后端动态请求的网络加速,也能大幅分摊源站的压力、支持更多请求同时访问,是性能提升的利器。
![[FDEIm91Zexcw5MhQ-672c709b.webp]]
腾讯云 CDN 产品文档中提供的 CDN 原理图:
![[OhMqUi79fp4zO8wo-31ae3dd3.webp]]
💡 如果你想了解一些云服务的介绍、架构和最佳实践,多去看大公司云服务的产品文档,就能学到很多知识。
CDN 请求的核心过程如下:
1. 图片文件由 **源站**(如 COS 对象存储、或者服务器)上传至 CDN 服务进行缓存。
2. 当用户请求图片时,CDN 会根据用户的地理位置,返回离用户 **最近的** CDN 节点缓存的图片资源。
3. 未命中缓存的图片将从源站获取,并缓存在 CDN 节点,供后续用户访问,俗称 **回源**。
💡 回源的具体解释:在内容分发网络中,回源是指用户通过浏览器发送请求时,响应该请求的是源站点的服务器,而不是各节点上的缓存服务器。一般情况下,当
CDN 节点上的缓存服务器没有缓存响应的内容,或者响应的内容在源站点服务器上被修改,就会回源站去获取。
#### 2、CDN 的优势
有同学会问了:COS 对象存储不也是存储图片的服务么?CDN 内容分发网络有啥独特的优势啊?
从这两个服务的名称中,我们就能明显感受到区别了,COS 更倾向于 “存储”,CDN 更倾向于 “网络请求”。
所以如果文件存储容量较大、但是访问频率较低,用对象存储性价比更高;但如果资源访问频率高、流量消耗大,还需要对访问进行加速、减少源站压力,就要使用
CDN 了。CDN 的流量和请求单价通常低于对象存储,而且更加安全,可以保护源站地址不被泄露。
#### 3、如何使用 CDN?
一般情况下,如果你要对外提供文件(图片)访问 / 下载服务,建议结合 COS 和 CDN。比如对于本项目,COS 作为源站,负责存储图片文件;CDN
负责提供文件访问服务,以及缓存、安全性的设置。
也就是说,使用 CDN 之后,我们数据库中存储的图片地址就不再是 COS 的地址,而是 CDN 的 URL。
如何开通和使用 CDN 服务?建议阅读 [官方的产品文档](https://cloud.tencent.com/document/product/228/3149)、还有
[CDN 结合 COS 的文档](https://cloud.tencent.com/document/product/228/38088),写得很贴心。
💡 CDN 还提供自动图片优化功能,感兴趣的同学可以 [看文档](https://cloud.tencent.com/document/product/228/43121) 了解下。
但是,注意 CDN 是个付费产品,按量计费,所以使用时有一些注意事项。俗话说得好(鱼皮说的):“乱用 CDN,钱包两行泪!”
大家一定要认真看:
1)缓存策略:为静态资源(如图片、CSS、JS)设置长期缓存时间,可以减少回源的次数和消耗。
2)防盗链:配置 Referer 防盗链保护资源,比如仅允许自己的域名可以加载图片
![[O3wZcjuCXbJKMKvm-da4111e3.webp]]
3)IP 限制:根据需要配置 IP 黑白名单,限制不必要的访问。
![[Wv6G8WLu4OgPz392-af9a6b93.webp]]
4)HTTPS 配置:配置有效的 SSL 证书,启用 HTTPS 传输,提高请求的安全性。
5)**监控告警:这点尤为重要!**一定要给 CDN 配置监控告警,比如设置一段时间内最多消耗的流量,超出时会自动发短信告警,避免费用超额;或者限制单个
IP 的请求频率,防止突发流量影响服务。
![[F6lRayECEMdsFdsd-fc06e00b.webp]]
6)CDN 节点选择:国内业务选择覆盖中国大陆的节点就足够了,非必要的话,不要开通全球 CDN 节点,容易遭受海外攻击。
7)访问日志:开启访问日志,分析用户行为和流量来源,这个能力更适合业务访问量较大的场景。
### 浏览器缓存
通过设置 HTTP 头信息(如 Cache-Control),可以让用户的浏览器将资源缓存在本地。在用户再次访问同样的资源时,直接从本地缓存加载资源,而无需再次请求服务器。
所有缓存在使用时的注意事项基本都是类似的:
1)设置合理的缓存时间。常用的几种设置参数是:
- 静态资源使用长期缓存,比如:`Cache-Control: public, max-age=31536000` 表示缓存一年,适合存储图片等静态资源。
- 动态内容使用验证缓存,比如:`Cache-Control: private, no-cache` 表示缓存可被客户端存储,但每次使用前需要与服务器验证有效性。适合会动态变化内容的页面,比如用户个人中心。
- 敏感内容禁用缓存,比如:`Cache-Control: no-store` 表示不允许任何形式的缓存,适合安全性较高的场景,比如登录页面、支付页面。
2)要能够及时更新缓存。可以给图片的名称添加 “版本号”(如文件名中包含 hash 值),这样哪怕上传相同的图片,由于版本号不同,得到的图片地址也不同,下次访问时就会重新加载。
对于我们的项目,图片资源是非常适合长期缓存在浏览器本地的,也已经通过给文件名添加日期和随机数防止了重复。由于图片是从对象存储云服务加载的,如果需要使用缓存,可以接入
CDN 服务,直接在云服务的控制台配置缓存,[参考文档](https://cloud.tencent.com/document/product/228/50114)。
![[4HnCuNdpEhyn1IOV-3921b517.webp]]
如果触发了浏览器本地缓存,在 F12 控制台中能够看到图片瞬间加载成功:
![[ljGci9GSV4qHP4rR-ab40b3e8.webp]]
## 四、图片存储优化
### 数据沉降
大部分数据的访问热度会随着存储时间延长逐渐降低,为了严格控制存储成本,需要定期分析业务数据的访问情况,并动态调整存储类型。
这就涉及到 **数据沉降** 技术,将长时间未访问的数据自动迁移到低频访问存储,从而降低存储成本。就跟我们平时使用电脑一样,SSD
硬盘很贵,我们一般优先将常用软件放在 SSD 目录中,至于一些以前写过的资料什么的,可以放在机械硬盘或外接硬盘中。
一般情况下,数据沉降分为 2 个阶段:先分析再沉降。
1)先分析:通过对象存储提供的清单 / 访问日志分析,或者业务代码中自行统计分析。
2)再沉降:可以直接通过对象存储提供的 **生命周期** 功能自动沉降数据,只需编写沉降规则即可。如下图,将 30 天未修改的文件沉降至低频存储:
![[3xmJpnBWOdJp29zP-f67e46fb.webp]]
低频存储的价格比标准存储低了一些,还可以将 **几乎不需要修改和访问** 的文件(比如日志文件)移动到归档存储中,存储价格更低,可节约几倍的成本!
![[JhJmyZB6YqpTTKgU-615a29bb.webp]]
不过要注意,虽然低频存储的存储费用更低,但是当你要访问低频存储的资源时,会产生数据取回费用,所以一般只对几乎不访问的资源进行沉降,尽量减少取回费用。
数据沉降和 **冷热数据分离** 的概念是比较接近的,冷热数据分离是根据数据的访问热度,将访问频繁的数据(热数据)和访问较少的数据(冷数据)存储在不同的存储层中。
对于我们的项目,很久无人问津的历史图片就可以称为 “冷数据”,可以利用 COS 的生命周期功能在 30 天后自动沉降为低频存储。当然也可以通过数据库记录图片的访问和下载时间,自行调用
API 批量沉降数据或者转储到其他存储服务。
💡 数据沉降和冷热数据分离的概念又有一些细微的差别,个人的理解是数据沉降更倾向于关注一个对象的生命周期(一个资源从热到冷),目标更多的是降低存储成本,配置沉降规则后也一般不会调整。冷热数据分离更关注整个系统的资源分布(比如热门图片放到性能更高的硬盘中,冷门图片进行归档存储),目标是同时优化性能和节约成本,数据的热度可以实时调整。
### 清理策略
对于 “重存储” 的系统,数据清理是必要的!通过设置合理的清理策略,可以避免冗余数据占用存储空间,降低成本。
这里鱼皮分享 4 种典型的清理策略:
1)立即清理:在删除图片记录时,立即关联删除对象存储中已上传的图片文件,确保数据库记录与存储文件保持一致。
这里还有个小技巧,可以使用异步清理降低对删除操作性能的影响,并且记录一些日志,避免删除失败的情况。
2)手动清理:由管理员手动触发清理任务,可以筛选要清理的数据,按需选择需要清理的文件范围。
3)定期清理:通过定时任务自动触发清理操作。系统预先设置规则(如文件未访问时间超过一定期限)自动清理不需要的数据。
4)惰性清理:清理任务不会主动执行,而是等到资源需求增加(存储空间不足)或触发特定操作时才清理,适合存储空间紧张但清理任务优先级较低的场景。
实际开发中,以上几种清理策略可以结合使用。**比如 Redis 的内存管理机制结合了定期清理和惰性清理策略。** 定期清理通过后台定期扫描一部分键,随机检查并删除已过期的键,从而主动释放内存,减少过期键的堆积。惰性清理则是在访问某个键时,检查其是否已过期,如果已过期则立即删除。这两种策略互为补充:定期清理降低了过期键的占用积累,而惰性清理确保了访问时键的准确性和及时清理,从而在性能和内存使用之间取得平衡。
#### 实现方案
对于我们的项目,由于不像 Redis 一样对空间的限制那么严格,更多的是为了节约成本,所以不需要惰性清理策略,按需运用 “立即清理 + 手动清理
+ 定期清理” 即可。
#### 后端开发
1)CosManager 补充删除对象的方法:
```java
/**
* 删除对象
*
* @param key 文件 key
*/
public void deleteObject(String key) throws CosClientException {
cosClient.deleteObject(cosClientConfig.getBucket(), key);
}
```
2)在 PictureService 中开发图片清理方法。
注意,删除图片时,需要先判断该图片地址是否还存在于其他记录里,确认没有才能删除。比如秒传的场景,就有可能多个图片地址指向同一个文件。
此外,还要注意删除对象存储中的文件时传入的是 key(不包含域名的相对路径),而数据库中取到的图片地址是包含域名的,所以删除前要移除域名,从而得到
key。这段鱼皮的视频教程中没有讲,大家可以自行实现。
```java
@Async
@Override
public void clearPictureFile(Picture oldPicture) {
// 判断该图片是否被多条记录使用
String pictureUrl = oldPicture.getUrl();
long count = this.lambdaQuery()
.eq(Picture::getUrl, pictureUrl)
.count();
// 有不止一条记录用到了该图片,不清理
if (count > 1) {
return;
}
// FIXME 注意,这里的 url 包含了域名,实际上只要传 key 值(存储路径)就够了
cosManager.deleteObject(oldPicture.getUrl());
// 清理缩略图
String thumbnailUrl = oldPicture.getThumbnailUrl();
if (StrUtil.isNotBlank(thumbnailUrl)) {
cosManager.deleteObject(thumbnailUrl);
}
}
```
上述代码中,使用了 Spring 的 `@Async` 注解,可以使得方法被异步调用,记得要在启动类上添加 `@EnableAsync` 注解才会生效。
```java
@SpringBootApplication
@EnableAsync
@MapperScan("com.yupi.yupicturebackend.mapper")
@EnableAspectJAutoProxy(exposeProxy = true)
public class YuPictureBackendApplication {
public static void main(String[] args) {
SpringApplication.run(YuPictureBackendApplication.class, args);
}
}
```
然后你可以将 clearPictureFile 方法运用到图片删除接口等场景。这里鱼皮给大家抛砖引玉,更多的清理策略大家可以自行实现。
#### 扩展
1)补充更多清理时机:在重新上传图片时,虽然那条图片记录不会删除,但其实之前的图片文件已经作废了,也可以触发清理逻辑。
2)实现更多清理策略:比如用 Spring Scheduler 定时任务实现定时清理、编写一个接口供管理员手动清理,作为一种兜底策略。
3)优化清理文件的代码,比如要删除多个文件时,使用 [对象存储的批量删除接口](https://cloud.tencent.com/document/product/436/65939#841fe310-bdf8-4789-9bc0-26ea844e316d) 代替
for 循环调用。
4)为了清理原图,可以在数据库中保存原图的地址。
---
至此,鱼皮智能协同云图库项目的第一阶段已经开发优化完成,大家已经可以将这个项目部署上线并写到简历上啦~ 从下一期教程开始,我们将继续升级平台的能力,让它能够满足更多使用需求。
## 五、其他
在进入下期教程前,大家可以运用自己学过的知识,对项目自行做一波优化。
比如之前我们每次重启服务器都要重新登陆,既然已经整合了 Redis,不妨使用 Redis 管理 Session,更好地维护登录态。
### Redis 分布式 Session
操作方式也很简单,1 分钟就能完成。
1)先在 Maven 中引入 spring-session-data-redis 库:
```xml
org.springframework.session
spring-session-data-redis
```
2)修改 application.yml 配置文件,更改 Session 的存储方式和过期时间:
```yaml
spring:
# session 配置
session:
store-type: redis
# session 30 天过期
timeout: 2592000
server:
port: 8123
servlet:
context-path: /api
# cookie 30 天过期
session:
cookie:
max-age: 2592000
```
这就搞定了,可以测试下重启服务器后是否还需要重新登陆,并且查看 Redis 中是否有登录相关的 key。
![[KhXOrZbydXn6Zfwu-2ea9627e.webp]]
---
**项目分区导航**:⬅️ [[05-redis与session|05-redis与session]] | 06-图片优化 | ➡️ [[07-图片模块|07-图片模块]]