首页实现多级缓存

发布于:2025-06-22 ⋅ 阅读:(17) ⋅ 点赞:(0)

缓存设计

缓存对象:需要缓存首先的图片列表数据,也就是对 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:

 <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!-- 本地缓存 Caffeine -->
<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>3.1.8</version>
</dependency>

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

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

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

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

private final Cache<String, String> LOCAL_CACHE =
        Caffeine.newBuilder().initialCapacity(1024)
                .maximumSize(10000L)
                // 缓存 5 分钟移除
                .expireAfterWrite(5L, TimeUnit.MINUTES)
                .build();
 @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());

        //构建缓存 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<PictureVO> cachePage = JSONUtil.toBean(cacheValue, Page.class);
            return ResultUtils.success(cachePage);
        }
        //2.本地缓存未命中,查询Redis 分布式缓存
        ValueOperations<String,String> valueOps = stringRedisTemplate.opsForValue();
        String cachedValue = valueOps.get(cacheKey);
        if(cachedValue != null){
            //如果缓存命中,更新本地缓存,返回结果
            LOCAL_CACHE.put(cacheKey,cachedValue);
            Page<PictureVO> cachePage = JSONUtil.toBean(cachedValue, Page.class);
            return ResultUtils.success(cachePage);
        }
        //3.查询数据库
        Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
                pictureService.getQueryWrapper(pictureQueryRequest));
        //获取封装类
        Page<PictureVO> 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);
    }

测试:

没有缓存

有缓存

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


网站公告

今日签到

点亮在社区的每一天
去签到