前言

  • 你是否不想再重复使用 RedisTemplate 来手动地来操纵缓存?
  • 你是否苦于不知道如何配置 Redis 配置类中的序列化器和反序列化器?
  • 你是否想如何才能得到纯净可读的 JSON 格式的 Redis value ?
  • 你是否想知道 Spring Cache 和 Redis 如何搭配使用实现注解式开发缓存?
  • 你是否想知道 Spring Cache 中有哪些注解?它们分别是怎么使用的?
    在这里插入图片描述
  • 恭喜你,找到宝藏了。本文将一站式教你解决上述所有问题。

缓存方式选择的考量

  • 目前我学习到的使用 Redis 缓存的方式有两种:
    • 【原生】使用 StringRedisTemplate 操纵 Redis ,再使用 Hutool 工具包手动地 (反) 序列化。好处是不需要写 RedisConfig 配置类,可以得到纯净的 JSON Value 。缺点是出现大量 Redis 的冗余代码。
    • 【Spring Cache】在业务层方法上添加注解即可自动完成缓存的增删查操作。优点是简化代码,缺点是需要写复杂的配置类。

  • 对于上面两种 Redis 缓存方式,选择哪一种,根据我浅薄的经验谈谈:

  • 对于业务逻辑简单,方法返回值就是要缓存的数据 value ,且方法的入参或者返回值适合作为 key 的业务方法,适合使用 Spring Cache 简化缓存开发。

  • 对于业务逻辑复杂、粒度更细、方法内部的某一中间值作为要缓存的 value ,那么 Spring Cache 就不太适合了。这时选择原生的 StringRedisTemplate 精准操纵 Redis + Hutool 工具包手动 (反) 序列化是更明智的选择。如下代码所示:

    // 注入StringRedisTemplate
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // (批量)套餐启售/停售
    @Override
    public Boolean updateStatus(Integer status, List<Long> ids) {
        
        ...省略;
        
        // 用原生方法以细粒度的方式删除该类套餐下的缓存
        // 构造keys
        Set<String> keys = setmeals.stream().map(setmeal -> {
            // 获取每个套餐的类别ID
            Long categoryId = setmeal.getCategoryId();
            // 使用Hutool工具包序列化成JSON字符串
            return JSONUtil.toJsonStr(categoryId) + "-1";
        }).collect(Collectors.toSet()); // 由于不能重复,因为使用Set集合收集结果
        
        // 使用StringRedisTemplate批量删除缓存
        redisTemplate.delete(keys);
    
        // 3.批量修改
        return this.updateBatchById(setmeals);
    }
    

1. Spring Cache is All You Need

  • 使用 Spring Cache 可以简化缓存优化的开发。

2. Spring Cache介绍

  • Spring Cache 是 Spring 提供的一整套的缓存解决方案,它不是具体的缓存实现,它只提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案,比如 Redis、Caffeine、Guava Cache、Ehcache。使用注解方式替代原有硬编码方式缓存,语法更加简单优雅!

  • Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。Spring Cache 提供了一层抽象,底层可以切换不同的 cache 实现。

  • Spring Cache 是通过 CacheManager 接口来统一不同的缓存技术。CacheManager 是 Spring 提供的各种缓存技术抽象接口。针对不同的缓存技术需要实现不同的 CacheManager:

    CacheManager描述
    EhCacheCacheManager使用EhCache作为缓存技术
    GuavaCacheManager使用Google的GuavaCache作为缓存技术
    RedisCacheManager使用Redis作为缓存技术

3. Spring Cache常用注解

注解功能添加位置
@EnableCaching开启缓存注解功能Spring Boot启动类
@Cacheable在方法执行前Spring先查看缓存中是否有数据。如果有数据,则直接返回缓存中的数据;若没有数据,调用方法并将方法返回值放到缓存中业务层的查询方法上
@CachePut将方法的返回值放到缓存中业务层的新增方法上
@CacheEvict将一条或多条数据从缓存中删除业务层的修改和删除方法上

1)@CachePut注解的使用

  • @CachePut 注解中有四个入参:
入参说明
String value配置缓存【分区】,相当于缓存的标示,每个缓存【分区】下可以有多个 key
String key配置缓存的【分区】下的具体表示,此处支持SpringEL 表达式 (后面会介绍),动态命名
String cacheManegerString 选择配置类中的缓存配置对象 beanname,不选走默认
String condition注解生效条件, 支持 SpringEL 表达式 例如: #result != null 结果不为null,才进行缓存
  • 为了动态地构建 key 的值,key 支持 Spring 表达式语言 SpringEL (Spring Expression Language) 。# 为 SpringEL 的开始标识,固定写法。
  • Spring Cache 的使用核心在于设计如何动态地计算 key 的值,下面总结了 key 的四种常用的构造方法:
key的写法说明
key = “#p0.id”把第[0]个入参的属性 id 作为key
key = “#user.id”把入参 User user 的属性 id 作为key
key = “#root.args[0].id”把第[0]个入参的属性 id 作为key
key = “#result.id”返回值 User user 的属性 id 作为key

  • 举例:下面的代码展示了 save() 方法,把返回值 user 的 ID 作为 key 。

    @CachePut(value = "userCache", key = "#result.id")
    @PostMapping
    public User user(User user) {
        userService.save(user);
        return user;
    }
    

2)@CacheEvict注解的使用

  • @CacheEvict 注解中常用的五个入参如下所示:
入参说明
String value配置缓存【分区】,相当于缓存的标示,每个缓存【分区】下可以有多个 key
String key配置缓存的【分区】下的具体表示,此处支持SpringEL 表达式 (后面会介绍),动态命名
String cacheManegerString 选择配置类中的缓存配置对象 beanname,不选走默认
String condition注解生效条件, 支持 SpringEL 表达式 例如: #result != null 结果不为null,才进行缓存
boolean allEntries默认为false;为true时删除 value 【分区】下的全部缓存数据

【注意】

  • 在我实际使用中,@CacheEvict 注解如果入参为 allEntries = true 时,必须放在控制层的方法上才有效,放在业务层方法上没有效果。

  • 前面介绍了 SpEL 动态地计算 key 的值,其实 SpEL 还支持多种不同的写法。下面三种 key 的 SpEL 都是等价的。

  • 举例 1 :下面的 @CacheEvict 中的 key#p0 。其中,# 为 SpEL 的开始标识,固定写法。而 p0 表示 @CacheEvict 注解所修饰的方法的第 [0] 个入参,即 Long id

    @CacheEvict(value = "userCache", key = "#p0")
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        userService.removeById(id);
    }
    
  • 举例 2 :下面的 @CacheEvict 中的 key#root.args[0] 。其中,# 为 SpEL 的开始标识,固定写法。而 root.args[0] 表示 @CacheEvict 注解所修饰的方法的第 [0] 个入参,即 Long id

    @CacheEvict(value = "userCache", key = "#root.args[0]")
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        userService.removeById(id);
    }
    
  • 举例 3 (推荐) :下面的 @CacheEvict 中的 key#id 。其中,# 为 SpEL 的开始标识,固定写法。而 id 表示 @CacheEvict 注解所修饰的方法的入参 Long id ,两者的名称必须相同。

    @CacheEvict(value = "userCache", key = "#id")
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        userService.removeById(id);
    }
    
  • 以上三种 key 的 SpEL 效果都是相同的。都是以方法的入参 Long id 作为缓存的 key 。


3)@Cacheable

  • @Cacheable 注解通常修饰查询的方法,但还支持条件判断参数 conditionunless

    • 只有满足 condition 中的条件时才缓存数据。
    • 满足 unless 中的条件则缓存。
  • 例如,可以支持当查询结果不为空的时候才缓存:

    @Cacheable(value = "userCache", key = "#id", unless = "#result == null")
    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        User user = userService.getById(id);
        return user;
    }
    

  • 举例:常见的分页查询。可以拼接 key :

    @Cacheable(value = "userCache", key = "#user.id + '_' + #user.name")
    @GetMapping("/list")
    public List<User> list(User user) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(user.getId() != nul1, User::getId, user.getId());
        queryWrapper.eq(user.getName() != nul1, User::getName, user.getName());
        List<User> list = userService.list(queryWrapper);
        return list;
    }
    

4)总结

  • Spring Cache 的使用核心在于设计如何动态地计算 key 的值,下面总结了 key 的四种常用的构造方法:
key的写法说明
key = “#p0.id”把第[0]个入参的属性 id 作为key
key = “#user.id”把入参 User user 的属性 id 作为key
key = “#root.args[0].id”把第[0]个入参的属性 id 作为key
key = “#result.id”返回值 User user 的属性 id 作为key

4. Spring Cache使用方式

  • Spring Cache 使用只需要三步:导入Maven依赖坐标、缓存配置、添加注解。
  • 在 Spring Boot 项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用 @EnableCaching 开启缓存支持即可。
  • 例如,使用 Redis 作为缓存技术,只需要导入Spring Data Redis 的Maven 坐标即可。

1)导入Maven坐标

  • 打开 pom.xml 文件,确保已经添加了以下依赖的坐标:

    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- common-pool Redis连接池依赖 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    
    <!-- Spring Cache -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    <!-- Jackson依赖 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    

2)修改Spring Boot配置文件

  • 打开 application.yml ,添加 Spring Cache 的相关配置:

    spring:  
      # 配置Redis连接
      redis:
        host: 192.168.148.100
        port: 6379
        password: xsh981104
        database: 0
        # 设置Lettuce Redis连接池
        lettuce:
          pool:
            max-active: 8 # 最大连接数
            max-idle: 8 # 最大空闲连接
            min-idle: 0 # 最小空闲连接
            max-wait: 100ms # 等待时长
      # 配置Spring Cache缓存
      cache:
        type: redis # 设置缓存技术使用Redis
        redis:
          time-to-live: 3600000 # 设置缓存有效期为60min
    

3)创建Redis配置类

  • 创建 src/main/java/edu/ouc/config/RedisConfig.java

  • Redis 配置类 RedisConfig.java 用于声明 Redis 缓存的序列化方式【JSON】,配置缓存时间等。

    @Configuration
    @EnableCaching  // 开启Spring Cache缓存注解
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    public class RedisConfig extends CachingConfigurerSupport {
    
        // 实例化具体的缓存配置类
        // 设置序列化方式为JSON
        // 设置缓存时间,单位为秒
        private RedisCacheConfiguration instanceConfig(Long ttl) {
    
            // 1.创建jackson的Redis缓存序列化器
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
    
    //        // 2.常见的Jackson的对象映射器,并设置一些基本属性
    //        // 2.1 创建Jackson的对象映射器对象
    //        ObjectMapper objectMapper = new ObjectMapper();
    //        // 2.2 在序列化过程中关闭把日期时间转换成时间戳
    //        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    //        // 2.3 注册Java的时间模块
    //        objectMapper.registerModule(new JavaTimeModule());
    //        // 2.4 禁用映射器注解
    //        objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false); // 被弃用
    //        // 2.5 设置JSON序列化器,且不为空时才序列化
    //        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    //        // 2.6 不懂
    //        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
    //                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    
            // 2.新版本Jackson推荐使用JsonMapper,替换上面的老版本的ObjectMapper
            JsonMapper jsonMapper = JsonMapper.builder()
                    .configure(MapperFeature.USE_ANNOTATIONS, false)
                    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                    .build();
            jsonMapper.registerModule(new JavaTimeModule());
            jsonMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            jsonMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                    ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    
            // 3.为序列化器设置对象映射器
            jackson2JsonRedisSerializer.setObjectMapper(jsonMapper);
    
            // 4.返回Redis缓存配置对象
            return RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(ttl))  // 设置缓存时间
                    .disableCachingNullValues()
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        }
    
        // 配置缓存Manager
        @Bean
        @Primary  //同类型多个bean时,默认生效! 默认缓存时间1小时!  可以选择!
        public RedisCacheManager cacheManagerHour(RedisConnectionFactory redisConnectionFactory) {
    
            RedisCacheConfiguration instanceConfig = instanceConfig(1 * 3600L); //缓存时间1小时
    
            //构建缓存对象
            return RedisCacheManager.builder(redisConnectionFactory)
                    .cacheDefaults(instanceConfig)
                    .transactionAware()
                    .build();
        }
    
        //缓存24小时配置
        @Bean
        public RedisCacheManager cacheManagerDay(RedisConnectionFactory redisConnectionFactory) {
    
            RedisCacheConfiguration instanceConfig = instanceConfig(24 * 3600L);    //缓存时间24小时
    
            //构建缓存对象
            return RedisCacheManager.builder(redisConnectionFactory)
                    .cacheDefaults(instanceConfig)
                    .transactionAware()
                    .build();
        }
    }
    

4)启动类开启缓存注解

  • 打开你的 Spring Boot 的启动类,添加打开缓存注解。重点看第 5 行代码:

    @Slf4j  // 日志
    @SpringBootApplication  // Spring Boot启动类
    @ServletComponentScan   // Servlet组件扫描,扫描过滤器
    @EnableTransactionManagement    // 开启Spring事务注解管理
    @EnableCaching  // 开启Spring Cache缓存
    public class ReggieTakeOutApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ReggieTakeOutApplication.class, args);
            // 打印Slf4j日志
            log.info("项目启动成功");
        }
    }
    

5)开始编码

  • 举例:黑马《瑞吉外卖》项目的套餐分页查询业务层方法,重点看第 3 行代码,直接添加注解 @Cacheable ,Spring Cache 就会先去 Redis 中查询是否有数据,如果有数据,则直接返回缓存中的数据;若没有数据,调用方法并将方法返回值放到缓存中。非常地方便好用。

  • 总而言之,Spring Cache + Redis 作为缓存时,思考的重点是 key 的设计,如何设计出区分度高、可读性高的 key 才是开发者需要认真思考的。

    // 根据条件查询套餐集合
    @Override
    @Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")
    public List<Setmeal> list(Setmeal setmeal) {
        // 1.创建查询条件封装器
        LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
        // 2.添加查询条件:根据类别ID查询
        lqw.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
        // 3.添加查询条件:根据售卖状态查询
        lqw.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
        // 4.调用数据层返回套餐对象构成的集合
        return this.list(lqw);
    }
    

6)Redis中缓存的数据展示

  • 缓存的 Value 值都是可读性很高的、纯净的 JSON 字符串:

    image-20221112211050511

  • Spring Cache + Redis 自动化缓存的方案就成功搭建起来了,大家赶快用起来吧。有问题可以欢迎评论和私信。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐