图片优化主要有以下四点:查询优化,上传优化,加载优化,存储优化。形成的逻辑链如下:
用户上传图片 → 后端处理/存储 → 前端查询图片 → 前端加载展示 → 长期存储维护
(上传优化) (存储优化) (查询优化) (加载优化) (存储优化)
图片查询优化
**方法:**主要使用缓存进行优化,降低数据库压力,提高系统性能。读多写少的情况适合缓存。
以缓存key设计-缓存value设计-缓存过期时间设置的思维链展开。
Redis 分布式缓存
节点 Redis 的读写 QPS 可达 10w 次每秒
缓存设计
缓存key设计
查询条件+项目业务前缀作为key的实现,再通过mad5哈希算法压缩JSON字符串
缓存value设计
选择合适的数据结构
从数据库中读取的Page分页对象->JSON格式字符串/二进制->redis的string数据结构
缓存过期时间设置
根据业务具体选择,为避免缓存雪崩可加入随机数
后端开发
引入依赖-redis配置-业务代码逻辑
java
@PostMapping("/list/page/vo/cache")
public BaseResponse<Page<PictureVO>> 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());
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
String redisKey = "yupicture:listPictureVOByPage:" + hashKey;
ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
String cachedValue = valueOps.get(redisKey);
if (cachedValue != null) {
Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
Page<PictureVO> pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
int cacheExpireTime = 300 + RandomUtil.randomInt(0, 300);
valueOps.set(redisKey, cacheValue, cacheExpireTime, TimeUnit.SECONDS);
return ResultUtils.success(pictureVOPage);
}
要点:
1.通过StringRedisTemplate操作redis
2.ValueOperations操作字符串
3.DigestUtils.md5DigestAsHex()生成hashkey
4.JSONUtil.toBean(cachedValue,Page.class)实现value与java对象转换(反序列化)
5.JSONUtil.toJsonStr()实现java对象转换成json字符串(序列化)
6.从50ms优化到30ms
Caffeine 本地缓存
将这些数据缓存到应用的内存中(比如 JVM 中),适用于数据访问量比较小的单机应用。
缓存设计
与分布式缓存类似,但由于在本地服务器内存,因此key应该再精简一点。
后端开发
引入依赖-构建本地缓存数据结构(容量与过期时间)-后端业务代码逻辑
由于和本地缓存流程相同(查缓存-命中返回-未命中查数据库-添加缓存),可使用模板方法进行优化
多级缓存
这部分在黑马点评里有更详细的介绍。

要点:
优化成果从50ms->10ms,提高80%。
其它查询优化策略
数据库查询优化,识别热点图片缓存
图片上传优化
**方法:**图片压缩,图片秒传、分片上传、断点续传
图片压缩
方案设计
压缩格式:
WebP格式与AVIF格式
AVIF格式兼容性不如WebP,但比WebP体积更小。
压缩方案:
本地图片处理类库or第三方云服务(数据万象)
数据万象可在访问时实时压缩也可上传图片时实时压缩。
后端开发
CosManager修改-处理存入Cos的逻辑
java
public PutObjectResult putPictureObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
PicOperations picOperations = new PicOperations();
picOperations.setIsPicInfo(1);
List<PicOperations.Rule> 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);
putObjectRequest.setPicOperations(picOperations);
return cosClient.putObject(putObjectRequest);
}
要点:
1.前缀修改为WebP
2.通过数据万象转换图片格式,设置规则
3.实际存储会存原图和压缩图两种。可选择删除原图
PictureUploadTemplate修改-处理存入数据库(前端的修改)
java
try {
file = File.createTempFile(uploadPath, null);
processFile(inputSource, file);
PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);
ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();
ProcessResults processResults = putObjectResult.getCiUploadResult().getProcessResults();
List<CIObject> objectList = processResults.getObjectList();
if (CollUtil.isNotEmpty(objectList)) {
CIObject compressedCiObject = objectList.get(0);
return buildResult(originFilename, compressedCiObject);
}
return buildResult(originFilename, file, uploadPath, imageInfo);
} catch (Exception e) {
log.error("图片上传到对象存储失败", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
}
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(cosClientConfig.getHost() + "/" + compressedCiObject.getKey());
return uploadPictureResult;
要点:
1.如果转换成功,则返回转换后的图片信息,否则返回原始图片信息
文件秒传
文件秒传是一种基于文件的唯一标识(如 MD5、SHA-256)对文件内容进行快速校验,避免重复上传的方法,在大型文件传输场景下非常重要。可以提高性能、节约带宽和存储资源。
原理:
1)客户端生成文件唯一标识:上传前,通过客户端计算文件的哈希值(如 MD5、SHA-256),生成文件的唯一指纹。
2)服务端校验文件指纹:后端接收到文件指纹后,在存储中查询是否已存在相同文件。
- 若存在相同文件,则直接返回文件的存储路径。
- 若不存在相同文件,则接收并存储新文件,同时记录其指纹信息。
java
String md5 = SecureUtil.md5(file);
List<Picture> pictureList = pictureService.lambdaQuery()
.eq(Picture::getMd5, md5)
.list();
if (CollUtil.isNotEmpty(pictureList)) {
Picture existPicture = pictureList.get(0);
} else {
}
为什么不用:
1.文件小,重复图片少,性能优化不明显
2.本项目使用腾讯云 COS 的对象存储,只能通过唯一地址去取文件,无法完全自定义文件的存储结构、也不支持文件快捷方式的概念,因此秒传的文件地址必须使用和原文件相同的对象路径,可能导致用户 A 上传的图片地址等同于用户 B 上传的地址。
分片上传和断点续传
分片上传: 将一个大文件 切割成多个固定大小的小分片,逐个上传到服务端,最后由服务端合并分片
断点续传: 基于分片上传,在上传中断后(网络断、客户端崩溃),下次上传时跳过已传成功的分片,只传未完成的
原理(腾讯云COS):
分片上传:
-
切割文件
- 客户端把大文件(比如 1GB 的视频)切割成多个小分片,比如按
1MB一个分片,最终切成 1000 个分片。 - 给每个分片编唯一序号(从 1 到 1000),同时计算整个文件的 MD5(用于校验文件完整性)、每个分片的 MD5(用于校验分片是否传错)。
- 客户端把大文件(比如 1GB 的视频)切割成多个小分片,比如按
-
初始化分片上传
- 客户端调用 COS 的
InitiateMultipartUploadAPI,告诉服务端:"我要上传一个大文件,文件名是 xxx,分片数量是 xxx"。 - 服务端返回一个 UploadId(分片上传的唯一标识),后续所有分片上传都要带这个 ID,服务端才知道这些分片属于同一个文件。
- 客户端调用 COS 的
-
并行上传分片
- 客户端并发上传 所有分片(比如同时传 5 个分片),每个分片上传时要带 3 个关键信息:
UploadId:分片所属的文件标识;PartNumber:分片序号(保证服务端合并顺序正确);- 分片的 MD5:服务端校验分片是否完整。
- 服务端接收分片后,会保存每个分片,并记录 "哪个 UploadId 的哪个序号分片已上传成功"。
- 客户端并发上传 所有分片(比如同时传 5 个分片),每个分片上传时要带 3 个关键信息:
-
合并分片
- 当所有分片都上传成功后,客户端调用 COS 的
CompleteMultipartUploadAPI,告诉服务端:"所有分片都传完了,麻烦合并成完整文件"。 - 服务端根据
UploadId找到所有对应的分片,按PartNumber顺序合并,生成最终文件;如果有分片丢失 / 损坏,会拒绝合并并提示错误。
- 当所有分片都上传成功后,客户端调用 COS 的
断点续传(基于分片的 "进度记录")
-
上传前:查询已传分片
- 客户端在上传大文件前,先调用 COS 的
ListPartsAPI,传入UploadId,查询:"这个文件之前已经传了哪些分片?" - 服务端返回已上传成功的分片序号列表(比如已传 1-500 分片)。
- 客户端在上传大文件前,先调用 COS 的
-
跳过已传分片,只传未完成的
- 客户端对比本地分片列表和服务端返回的列表,只上传未传的分片(比如 501-1000 分片)。
- 如果是第一次上传,服务端返回空列表,就正常上传所有分片。
-
上传中断:自动保存进度
- 上传过程中如果网络断开 / 客户端崩溃,已上传的分片会保存在服务端,进度不会丢失。
- 下次上传时重复步骤 1-2,继续未完成的部分。
为什么不用
文件较小,性能优化不明显
图片加载优化
**方法:**缩略图、懒加载、CDN 加速、浏览器缓存
缩略图
上传图片时,同时生成一份较小尺寸的缩略图。用户浏览图片列表时加载缩略图,只有在进入详情页或下载时才加载原图。
方案设计:
与图片压缩类似,可使用本地图像处理or第三方云服务
第三方云服务就是使用数据万象增添规则。
后端开发:
增加缩略图url字段-CosManager修改-PictureUploadTemplate修改
要点:
1**.**仅对 > 20 KB 的图片生成缩略图
2.如果没有生成缩略图,则缩略图等于压缩图
懒加载
后端:优化分页
前端:
1)使用 HTML5 原生的 loading="lazy" 属性。
2)使用 JS 的 Intersection Observer,这个 API 能够检测元素是否进入视口,参考实现如下:
- 将图片的真实 src 替换为一个占位属性(如 data-src)。
- 使用 Intersection Observer 监听图片是否进入视口。
- 当图片进入视口时,将 data-src 的值赋给 src,触发加载。
3)使用 JS 监听页面滚动事件实现。每次页面滚动时,判断图片是否进入可视区域;如果是,则给图片增加 src 属性,触发图片加载。
4)使用现成的组件库或类库实现,比如 lazysizes 库。
渐进式加载:和懒加载技术类似,先加载低分辨率或低质量的占位资源(如模糊的图片缩略图),在用户访问或等待期间逐步加载高分辨率的完整资源,加载完成后再替换掉占位资源。
CDN 加速
CDN(内容分发网络)是通过将图片文件分发到全球各地的节点,用户访问时从离自己最近的节点获取资源的技术,常用于文件资源或后端动态请求的网络加速,也能大幅分摊源站的压力、支持更多请求同时访问,是性能提升的利器。

- 图片文件由 源站(如 COS 对象存储、或者服务器)上传至 CDN 服务进行缓存。
- 当用户请求图片时,CDN 会根据用户的地理位置,返回离用户 最近的 CDN 节点缓存的图片资源。
- 未命中缓存的图片将从源站获取,并缓存在 CDN 节点,供后续用户访问,俗称 回源。
如何使用:在腾讯云开通就好了,CDN 结合 COS 的文档
最佳实践方案:
1)缓存策略:为静态资源(如图片、CSS、JS)设置长期缓存时间,可以减少回源的次数和消耗。
2)防盗链:配置 Referer 防盗链保护资源,比如仅允许自己的域名可以加载图片(在COS里设置)
3)IP 限制:根据需要配置 IP 黑白名单,限制不必要的访问。
4)HTTPS 配置:配置有效的 SSL 证书,启用 HTTPS 传输,提高请求的安全性。
5)** 监控告警:这点尤为重要!** 一定要给 CDN 配置监控告警,比如设置一段时间内最多消耗的流量,超出时会自动发短信告警,避免费用超额;或者限制单个 IP 的请求频率,防止突发流量影响服务。
6)CDN 节点选择:国内业务选择覆盖中国大陆的节点就足够了,非必要的话,不要开通全球 CDN 节点,容易遭受海外攻击。
7)访问日志:开启访问日志,分析用户行为和流量来源,这个能力更适合业务访问量较大的场景。
浏览器缓存
**强缓存:**给资源设置 "保质期",过期前直接用本地缓存,完全不请求服务器
协商缓存资源过期后,浏览器带着 "资源标识" 问服务器「这个资源更新了吗?」,没更新就继续用本地缓存,更新了才下载新资源
**具体实现:后端配置,**腾讯云 COS/CDN 配置缓存规则
后端配置
java
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CacheControlInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 只对图片等静态资源设置缓存
String requestURI = request.getRequestURI();
if (requestURI.endsWith(".jpg") || requestURI.endsWith(".png") || requestURI.endsWith(".webp")) {
// 强缓存:设置图片缓存30天(2592000秒)
response.setHeader("Cache-Control", "public, max-age=2592000");
// 兼容HTTP 1.0的Expires(可选)
response.setHeader("Expires", String.valueOf(System.currentTimeMillis() + 2592000 * 1000));
// 协商缓存:设置ETag(基于文件内容的哈希值)
// 可以用Spring的ShallowEtagHeaderFilter自动生成,或自己计算文件MD5
}
return true;
}
}
拦截器中设置Cache-Control。

这个浏览器中f12的禁用缓存是什么?
只是浏览器本身设置,如果不开启浏览器默认启发式缓存不可靠,因此需要开发者在后端或COS配置增添缓存规则。
图片存储优化
方法:数据沉降,清理策略
数据沉降
将长时间未访问的数据自动迁移到低频访问存储,从而降低存储成本。
数据沉降和 冷热数据分离 的概念是比较接近的,冷热数据分离是根据数据的访问热度,将访问频繁的数据(热数据)和访问较少的数据(冷数据)存储在不同的存储层中。
通过腾讯云cos的生命周期实现
清理策略
1)立即清理:在删除图片记录时,立即关联删除对象存储中已上传的图片文件,确保数据库记录与存储文件保持一致。
这里还有个小技巧,可以使用异步清理降低对删除操作性能的影响,并且记录一些日志,避免删除失败的情况。
2)手动清理:由管理员手动触发清理任务,可以筛选要清理的数据,按需选择需要清理的文件范围。
3)定期清理:通过定时任务自动触发清理操作。系统预先设置规则(如文件未访问时间超过一定期限)自动清理不需要的数据。
4)惰性清理:清理任务不会主动执行,而是等到资源需求增加(存储空间不足)或触发特定操作时才清理,适合存储空间紧张但清理任务优先级较低的场景
后端实现
CosManager 补充删除对象的方法-在 PictureService 中开发图片清理方法
要点:
1.判断该图片地址是否还存在于其他记录里,确认没有才能删除。
2.@Async 注解,可以使得方法被异步调用,启动类上添加 @EnableAsync 注解才会生效。
Redis 分布式 Session
把session保存到Redis里
依赖-配置
这里需要USER类实现序列化接口。