跳到主要内容基于 Redis+Caffeine+ 腾讯云的图片库查询上传加载存储优化与分布式 Session 登录 | 极客日志JavaSaaSjava算法
基于 Redis+Caffeine+ 腾讯云的图片库查询上传加载存储优化与分布式 Session 登录
通过 Redis 与 Caffeine 构建多级缓存提升查询性能,利用腾讯云对象存储实现 WebP 压缩与缩略图生成,结合 CDN 加速与浏览器缓存优化加载体验,并通过分布式 Session 管理登录态。


图片优化技术
在云图库项目上线前,针对图片处理进行了深度优化。主要涵盖以下四个维度:
- 图片查询优化:分布式缓存、本地缓存、多级缓存
- 图片上传优化:压缩、秒传、分片上传、断点续传
- 图片加载优化:懒加载、缩略图、CDN 加速、浏览器缓存
- 图片存储优化:降频存储(冷热数据分离)、清理策略
一、图片查询优化
缓存机制
对于高频访问的数据,直接从数据库获取效率较低。利用性能更高的存储介质作为缓存,可以显著降低数据库压力并提升系统响应速度。
适合缓存的数据通常具备'读多写少'的特征,例如:
- 高频访问数据:如首页推荐内容。
- 计算成本较高数据:如复杂统计结果。
- 允许短暂延迟数据:如排行榜、图片列表。
本项目中,主页及图片列表接口属于高频访问场景,且对实时性要求不高,非常适合引入缓存。
Redis 分布式缓存
分布式缓存将数据存储于多台服务器,适用于高并发场景。Redis 因其高性能和丰富的数据结构成为主流选择。
- 高性能:基于内存操作,单节点读写 QPS 可达 10 万。
- 丰富结构:支持字符串、列表、哈希等。
- 分布式支持:通过 Cluster 或哨兵模式实现高可用与扩展。
缓存设计
针对 listPictureVOByPage 接口进行缓存设计,遵循 key、value、过期时间三要素。
(1) 缓存 Key 设计
由于查询条件不同,需将其纳入 Key。为避免 JSON 过长,可使用 MD5 哈希压缩。同时添加项目前缀隔离业务。
yupicture:listPictureVOByPage:${查询条件 key}
(2) 缓存 Value 设计
存储分页对象 Page。为了可读性可转为 JSON 字符串,为了空间压缩可选用二进制,Redis 底层均为 String 结构。
(3) 过期时间设置
必须设置过期时间以防缓存堆积。考虑到图片持续更新,设置为 5~60 分钟较为合适。为了解决缓存雪崩问题,建议设置随机过期时间区间。
后端开发
Spring Boot 推荐使用 Spring Data Redis 集成,默认使用 Lettuce 客户端。
(1) 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
在 application.yml 中添加 Redis 配置:
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
timeout: 5000
使用 StringRedisTemplate 测试基础增删改查操作。
@SpringBootTest
public class RedisStringTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testRedisStringOperations() {
ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
String key = "testKey";
String value = "testValue";
valueOps.set(key, value);
assertEquals(value, valueOps.get(key));
valueOps.set(key, "updatedValue");
assertEquals("updatedValue", valueOps.get(key));
stringRedisTemplate.delete(key);
assertNull(valueOps.get(key));
}
}
注入 StringRedisTemplate,在查库前先查缓存。若命中直接返回,未命中则查库并回填缓存。
@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 = String.format("yupicture:listPictureVOByPage:%s", hashKey);
ValueOperations<String, String> opsedForValue = stringRedisTemplate.opsForValue();
String cachedValue = opsedForValue.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 cachedExpireTime = 300 + RandomUtil.randomInt(0, 300);
opsedForValue.set(redisKey, cacheValue, cachedExpireTime, TimeUnit.SECONDS);
return ResultUtils.success(pictureVOPage);
}
测试表明,引入缓存后平均响应时间显著下降。注意序列化过程中 null 字段可能被过滤,导致前后端数据结构略有差异。
Caffeine 本地缓存
本地缓存将数据存储在 JVM 内存中,速度更快但无法跨节点共享。适用于小型数据集或单机高频访问场景。
Caffeine 是 Java 生态中主流的本地缓存库,支持容量控制、过期策略和异步操作。
后端开发
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.1.8</version></dependency>
private final Cache<String, String> LOCAL_CACHE = Caffeine.newBuilder()
.initialCapacity(1024)
.maximumSize(10000L)
.expireAfterWrite(5L, TimeUnit.MINUTES)
.build();
参考分布式缓存逻辑,将 Redis 操作替换为 LOCAL_CACHE.getIfPresent 和 put。
多级缓存
结合本地缓存的高性能和分布式缓存的一致性,构建两级缓存系统。
- 第一级(Caffeine):优先读取,命中直接返回。
- 第二级(Redis):未命中则查 Redis,命中则回写本地缓存。
- 数据库:均未命中则查库,结果同时写入 Redis 和本地缓存。
这种架构提升了容错性,即使 Redis 故障,本地缓存仍可提供部分服务。
代码实现
String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey);
if (cachedValue != null) {
return ResultUtils.success(JSONUtil.toBean(cachedValue, Page.class));
}
cachedValue = valueOps.get(cacheKey);
if (cachedValue != null) {
LOCAL_CACHE.put(cacheKey, cachedValue);
return ResultUtils.success(JSONUtil.toBean(cachedValue, Page.class));
}
Page<Picture> picturePage = pictureService.page(...);
Page<PictureVO> pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
LOCAL_CACHE.put(cacheKey, cacheValue);
valueOps.set(cacheKey, cacheValue, 5, TimeUnit.MINUTES);
return ResultUtils.success(pictureVOPage);
常见问题与扩展
- 缓存击穿:热点数据过期时大量请求打库。解决方案:互斥锁或超长过期时间。
- 缓存穿透:查询不存在的数据。解决方案:布隆过滤器或缓存空值。
- 缓存雪崩:大量缓存同时过期。解决方案:随机过期时间或多级缓存。
二、图片上传优化
图片压缩
压缩能显著减少文件体积,降低带宽成本。推荐转换为 WebP 格式,相比 JPEG/PNG 体积更小且支持透明通道。
方案选择
利用腾讯云 COS 对象存储的数据万象服务,可在上传时自动处理图片规则,无需额外开发。
后端开发
在上传请求中增加图片处理规则,将后缀转为 .webp。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, key, file);
PicOperations picOperations = new PicOperations();
picOperations.setIsPicInfo(1);
List<PicOperations.Rule> rules = new ArrayList<>();
PicOperations.Rule compressRule = new PicOperations.Rule();
compressRule.setFileId(webpKey);
compressRule.setRule("imageMogr2/format/webp");
compressRule.setBucket(bucket);
rules.add(compressRule);
picOperations.setRules(rules);
putObjectRequest.setPicOperations(picOperations);
从上传结果中提取压缩后的图片信息,包括宽高、大小、格式等。
ProcessResults processResults = putObjectResult.getCiUploadResult().getProcessResults();
List<CIObject> objectList = processResults.getObjectList();
if (CollUtil.isNotEmpty(objectList)) {
CIObject compressCiObject = objectList.get(0);
return buildResult(originalFilename, compressCiObject);
}
根据压缩对象构建 UploadPictureResult,确保前端获取到的是 WebP 地址。
测试显示,压缩效果显著,且原图与压缩图同名便于管理。
文件秒传
基于文件指纹(MD5)校验,避免重复上传。虽然本项目因 COS 限制未完全实现,但原理通用:
- 客户端计算文件 MD5。
- 服务端查询是否存在该 MD5。
- 存在则直接返回路径,不存在则上传。
分片上传与断点续传
大文件传输建议使用 SDK 提供的分片功能,记录进度中断后可恢复,无需自行开发底层逻辑。
三、图片加载优化
缩略图
首页直接加载原图会导致加载慢、流量浪费。解决方案是在上传时生成缩略图,列表页仅加载缩略图。
实现步骤
- 数据库变更:
picture 表新增 thumbnailUrl 字段。
- MyBatis 映射:XML 中增加字段映射。
- 实体类同步:
Picture, PictureVO 等类增加对应属性。
- 处理逻辑:在上传规则中增加缩放参数
/thumbnail/128x128>。
注意:小图片生成缩略图可能反而变大,建议仅对大于 20KB 的图片生成缩略图。若无缩略图,则复用压缩图 URL。
CDN 加速
CDN(内容分发网络)将资源分发至全球节点,用户就近访问,大幅降低源站压力。
- 源站(COS)上传资源至 CDN。
- 用户请求命中 CDN 节点缓存,未命中则回源。
- 缓存策略:静态资源设置长期缓存。
- HTTPS:启用 SSL 证书保障安全。
- 防盗链:配置 Referer 白名单。
- 监控告警:设置流量阈值,防止费用超额。
浏览器缓存
通过 HTTP 头信息(如 Cache-Control)让浏览器缓存资源。配合 CDN 配置,可实现本地瞬间加载。
四、图片存储优化
数据沉降与清理
随着时间推移,数据热度降低。可通过生命周期规则将冷数据迁移至低频存储,降低成本。
- 立即清理:删除记录时同步删除文件。
- 定期清理:定时任务扫描过期数据。
- 手动清理:管理员触发特定范围清理。
后端开发
利用 Spring 的 @Async 异步删除文件,避免阻塞主线程。
@Async
public void clearPictureFile(Picture oldPicture) {
String pictureUrl = oldPicture.getUrl();
Long count = this.lambdaQuery().eq(Picture::getUrl, pictureUrl).count();
if (count <= 1) {
cosManager.deleteObject(pictureUrl);
if (StrUtil.isNotBlank(oldPicture.getThumbnailUrl())) {
cosManager.deleteObject(oldPicture.getThumbnailUrl());
}
}
}
五、登录态自动保持
重启服务器后重新登录体验不佳,可利用 Redis 管理 Session 实现分布式登录态保持。
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency>
spring:
session:
store-type: redis
timeout: 2592000
server:
servlet:
session:
cookie:
max-age: 2592000
配置完成后,重启服务无需重新登录,Session 信息持久化在 Redis 中。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online