缓存的实现方式

背景

  • 之前提到过在数据库查询中, 我们可以使用缓存来提高查询效率, 减少数据库的压力.

缓存的方式

  1. 本地缓存

    • 基于内存的缓存, 如HashMap, LinkedHashMap, Guava Cache等.
  2. 远程缓存

    • 分布式缓存: 如Redis, Memcached, Etcd(云原生架构的分布式缓存)等.
    • Java进程缓存: 如Ehcache(单机), Caffeine(号称最快的缓存库), Google Guava等.

Redis缓存

  • 在分布式缓存中, Redis是最常用的缓存服务器.
  • 了解redis的基本概念和使用方法, 可以查看文章: redis知识

使用Redis缓存注意事项

  1. 设计缓存的key

    • 一般来说, 我们需要缓存的数据的key应该是唯一的, 并且能够反映出数据的内容.
    • 格式: systemId:moduleId:func:options(系统ID:模块ID:功能:参数, 可以根据实际情况进行调整, 原则是不要和其他数据冲突)
  2. 缓存的过期时间

    • 一定要设置过期时间, redis的内存不能无限扩大, 所以需要设置合理的过期时间.
    • 缓存的过期时间应该根据数据的重要性和更新频率来设置.
    • 对于不经常更新的数据, 可以设置较短的过期时间, 如10分钟.
    • 对于经常更新的数据, 可以设置较长的过期时间, 如1天.
    • 对于一些重要的数据, 可以设置更长的过期时间, 如30天.

问题延伸

  1. 问题描述

    • redis缓存解决了查询效率的问题, 但是缓存的命中率并不一定高, 比如缓存删除之后,第一个查询还是会到数据库查询, 数据量大的话, 用户还是会看到延迟.
  2. 解决方案

    • 可以使用缓存预热

缓存预热

  • 缓存预热是指将热点数据提前加载到缓存中, 这样当第一个用户请求到来时, 缓存就有数据可以直接返回, 避免了延迟.
  1. 缓存预热的优点

    • 降低了首次加载的延迟, 提高了用户体验
    • 缓存预热后, 缓存命中率提高, 降低了数据库的压力
  2. 缓存预热的缺点

    • 增加开发成本, 需要额外的开发、设计
    • 预热的时机和时间如果设置不当, 可能会造成缓存数据不正确.
    • 缓存预热的缓存数据量不能太大, 否则会占用过多的内存.
  3. 缓存预热的实现方式

    • (1) 定时缓存预热: 定时任务定期访问数据库, 将热点数据加载到缓存中.
    • (2) 模拟触发(手动触发): 在代码中主动访问数据库, 将热点数据加载到缓存中.
  4. 定时缓存预热的实现方式

    • (1) Spring Scheduler(spring boot 默认整合了, 最常用)
    • (2) Quartz(独立于 Spring 存在的定时任务框架)
    • (3) XXL-Job 之类的分布式任务调度平台(界面 + sdk), 非常值得学习, https://github.com/xuxueli/xxl-job

使用Spring Scheduler实现定时缓存预热

  1. 主类添加注解@EnableScheduling

    1
    2
    3
    4
    5
    6
    7
    
      @EnableScheduling // 开启定时任务
      public class MainApplication {
    
        public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
      }
    }
    
  2. 给要定时执行的方法添加 @Scheduling 注解,指定 cron 表达式或者执行频率

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    @Component
    @Slf4j
    public class PreCacheJob {
    
      @Resource
      private UserService userService;
    
      @Resource
      private RedisTemplate<String, Object> redisTemplate;
    
      // 重点用户
      private List<Long> mainUserList = Arrays.asList(1L);
    
      // 每天执行,预热推荐用户
      @Scheduled(cron = "0 45 14 * * *")   //自己设置时间测试
      public void doCacheRecommendUser() {
          //查数据库
          for (Long userId : mainUserList) {
              QueryWrapper<User> queryWrapper = new QueryWrapper<>();
              Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
              String redisKey = String.format("hjx:user:recommend:%s", userId);
              ValueOperations valueOperations = redisTemplate.opsForValue();
              //写缓存,30s过期
              try {
                  valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
              } catch (Exception e) {
                  log.error("redis set key error", e);
              }
          }
      }
    }
    
使用 Hugo 构建
主题 StackJimmy 设计