本文介绍了图片列表数据缓存的设计与实现方案。采用多级缓存架构,结合Caffeine本地缓存和Redis分布式缓存,提升系统性能。缓存设计包括:1)使用MD5压缩查询条件作为Redis缓存key;2)缓存value存储为JSON格式;3)设置5-60分钟随机过期时间防止雪崩。建立时先查询本地缓存,未命中再查Redis,最后查询数据库,并将结果写入两级缓存。测试表明缓存显著提升访问速

缓存设计

缓存对象:需要缓存首先的图片列表数据,也就是对 listPictureVOByPage 接口进行缓存。

缓存三要素:“key、value、过期时间”。

1)缓存 key 设计

由于接口支持传入不同的查询条件,对应的数据不同,因此需要将查询条件作为缓存 key 的一部分。可以将查询条件对象转换为 JSON 字符串,但这个 JSON 会比较长,可以利用哈斯算法(md5) 来压缩 key。此外由于使用分布式缓存,可能由多个项目和业务共享,因此需要在 key 的开头拼接前缀进行隔离。设计出的 key 如下:

picture:listPictureVOByPage:${查询条件key}

2)缓存 value 设计

缓存从数据库中查到的 Page 分页对象,存储为什么格式呢?这里有2中选择:

  • 为了可读性,可以转换为 JSON 结构的字符串 。例:{"id":"1","name":"pgs"}
  • 为了压缩空间,可以存为二进制等其他结构

但是对应的 Redis 数据结构都是 string.

3)缓存过期的时间设置

必须设置缓存过期时间! 根据实际业务场景和缓存空间的大小、数据的一致性的要求设置,合适即可,此处由于查询条件较多、而且考虑到图片会持续更新,设置为 5 ~ 60 分钟即可。

如何操作 Redis?

Java 中有很多的 Redis 操作库,比如 Jedis、Lettuce 等。为了便于和 Spring 项目集成,Spring还提供了 Spring Data Redis 作为操作 Redis 的更高层抽象(默认使用 Lettuce 作为底层客户端)。由于我们的项目使用 Spring Boot,也推荐使用 Spring Data Redis,开发成本更低。

Caffeine 本地缓存

当应用需要频繁访问某些数据时,可以将这些缓存存到应用内存中(比如 JVM中);下次访问时,直接从内存读取,而不需要经过网络或其他存储系统。

相比于分布式缓存,本地缓存的速度更快,但是无法在多个服务器间共享数据、而且不方便扩容。

所以本地缓存的应用场景一般是:

  • 数据访问量有限的小型数据集
  • 不需要服务器间共享数据的单机应用
  • 高频、低延迟的访问场景(如用户临时会话信息、短期热点数据)。

对于 Java 项目,Caffeine 是主流的本地缓存技术,拥有极高的性能和丰富的功能。比如可以精确控制缓存数量和大小、支持缓存过期、支持多种缓存淘汰策略、支持异步操作、线程安全等。

多级缓存

多级缓存是指结合本地缓存和分布式缓存的优点,在同一业务场景下构建两级缓存系统,这样可以兼顾本地缓存的高性能、以及分布式缓存的数据一致性和可靠性。

多级缓存的工作流程:

  1. 第一级(Caffeine 本地缓存):优先从本地缓存中读取数据。如果命中,则直接返回。
  2. 第二级(Redis 分布式缓存):如果本地缓存未命中,则查询 Redis 分布式缓存。如果 Redis 命中,则返回数据并更新本地缓存。
  3. 数据库查询:如果 Redis 也没有命中,则查询数据库,并将结果写入 Redis 和本地缓存。

多级缓存还有‎一个优势,就是提升了系统的容错性。即使 Re⁢dis 出现故障,本地‍缓存仍可提供服务,减少⁠对数据库的直接依赖。

后端开发

1)引入 Maven 依赖,使用 Spring Boot Stater 快速整合 Redis,引入Caffeine:

org.springframework.boot
spring-boot-starter-data-redis
com.github.ben-manes.caffeine
caffeine
3.1.8

2)在 application.yml 中添加 Redis 配置:

spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
timeout: 5000

3)新写一个使用缓存的分页查询图片列表的接口。在查询数据库前先查询缓存,如果已有数据则直接返回缓存,如果没有数据则查询数据库,并且将结果设置到缓存中。

构造本地缓存,设置缓存容量和过期时间:

private final Cache LOCAL_CACHE =
Caffeine.newBuilder().initialCapacity(1024)
.maximumSize(10000L)
// 缓存 5 分钟移除
.expireAfterWrite(5L, TimeUnit.MINUTES)
.build();
@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 cacheKey = String.format("pgspicture:listPictureVOByPage:%s", hashKey);
//从 Redis 缓存中查询
//1.先从本地缓存中查询
String cacheValue = LOCAL_CACHE.getIfPresent(cacheKey);
if(cacheValue != null){
//如果缓存命中,返回结果
Page cachePage = JSONUtil.toBean(cacheValue, Page.class);
return ResultUtils.success(cachePage);
}
//2.本地缓存未命中,查询Redis 分布式缓存
ValueOperations valueOps = stringRedisTemplate.opsForValue();
String cachedValue = valueOps.get(cacheKey);
if(cachedValue != null){
//如果缓存命中,更新本地缓存,返回结果
LOCAL_CACHE.put(cacheKey,cachedValue);
Page cachePage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachePage);
}
//3.查询数据库
Page picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
//获取封装类
Page pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
//4.更新缓存
//存入 Redis 缓存
LOCAL_CACHE.put(cacheKey,cacheValue);
cacheValue = JSONUtil.toJsonStr(pictureVOPage);
// 5 - 10 分钟随机过期,防止雪崩
int cacheExpireTime = 300 + RandomUtil.randomInt(0,300);
valueOps.set(cacheKey,cacheValue,cacheExpireTime, TimeUnit.SECONDS);
//返回结果
return ResultUtils.success(pictureVOPage);
}

测试:

没有缓存

有缓存

有缓存的情况下明显访问速度快了十来倍。

posted @ 2025-07-25 13:27  wzzkaifa  阅读(18)  评论(0)    收藏  举报