spring cache 学习——整合 redis 实现声明式缓存配置

前言:

  本文只是介绍怎么使用,关于一些源码的解析,请看另一篇:https://www.cnblogs.com/coding-one/p/12373522.html

1. 添加依赖(版本自选)

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
                <version>2.2.4.RELEASE</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>2.2.4.RELEASE</version>
            </dependency>

依赖

2. 直接使用

  spring 默认为我们提供了两个操作 redis 的 bean,分别是:

@Autowired
    RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

  所以,我们只需要在需要用到的地方注入即可使用,例子:

@GetMapping("/getById/{id}")
    public Menu getById(@PathVariable("id")String id){
        String menuName = stringRedisTemplate.opsForValue().get("menuName_" + id);
        Menu menu = null;
        if (menuName == null){
            menu = menuService.getById(id);
            menuName = menu.getName();
            stringRedisTemplate.opsForValue().set("menuName_" + id, menuName);
        }
        menu = (Menu)redisTemplate.opsForValue().get("menu_" + id);
        if (menu == null){
            menu = menuService.getById(id);
            redisTemplate.opsForValue().set("menu_" + id, menu);
        }
        return menu;
    }

  2.1. 在测试之前,我们先来看看当前现有的 key:

    

   2.2. 接下来我们请求上面代码中的接口,结果如下:

    

   2.3. 按我们代码的逻辑,这个时候 redis 中应该多了两条记录,我们来看看:

    name 字段的缓存记录:

      

 

 

     整个 menu 的缓存记录:

      

  2.4. 接下来,我们手动把 mysql 中的数据字段改一下,再重新请求一遍,按照代码逻辑,期望的返回结果应该跟第一次请求完全一样。

    使用 sqlyog 修改 mysql 中的数据:

      

    重新请求一遍,结果:

      

    可以看到,结果与期望一致。到目前为止,我们证明了“若 redis 缓存中有数据,就不会去数据库查询”。接下来我们来证明“若 redis 缓存中没有数据,则去查询数据库”。我们手动删掉 redis 中上述两个 key,重新请求一遍,结果:

      

  至此,缓存的功能已经实现了。

 

3. 自定义序列化配置

  从上面例子中 RDM 工具的截图可以看到,name 字段的键值存储是正常的,但是 menu 对象的键值都是乱码的(准确的说不叫乱码,而是二进制数据的十六进制表示),这样的内容可读性非常差,不友好。我们知道,redis 只支持 string 和基于 string 的几种集合数据类型,并不能存储 java 对象。所以我们将 Menu 对象从 redis 中进行存取的操作肯定经过了对象的序列化和反序列化过程。所以十六进制存储肯定是序列化的结果,所以我们如果想要存储的内容可读性强,只需要指定序列化的方式(比如指定为 json 序列化)即可。spring 当然为我们考虑到了,spring 允许我们自定义 RedisTemplate 的 bean,来覆盖默认的。

  3.1. 我们新建一个配置类,然后使用   @Bean  来创建一个 RedisTemplate 对象,交给容器管理。我们在创建 bean 的时候,可以指定序列化器

@Configuration
public class OnezaiRedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Object> redisTemplate  = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        StringRedisTemplate redisTemplate  = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        return redisTemplate;
    }

  在这里,我们将 value 序列化器指定为   GenericJackson2JsonRedisSerializer  。这是 spring-data-redis 自带的一个 json 序列化器。现在我们再来看看新的序列化结果。

  3.2. 删掉刚刚写进 redis 的 key,重启,请求接口,然后查看 redis 中的内容:

    

 

     可以看到,存储的 key 变为正常的 string ,value 变成了 json。

  这里我们使用的是自带的序列化器,序列化之后的结果可读性强了很多。但是加入了 class 信息,而且时间类型使用了时间戳,可读性还是不够。如果你还是不满足,那么可以自定义序列化器,重写序列化方法(比如使用 fastjson)这里不做例子了。

 

4. 做一个小封装

  首先我们回头看一下前面的代码,我们在使用的时候调用了一个 opsForValue 方法。其实 RedisTemplate 是一个统一的 dao 类,它自己本身只提供 key 的操作方法,但是它内部引用了不同的操作器类(xxxOperations)来分别操作不同的 redis 数据类型,具体的有:

    

  这样一来我们在调用的时候都需要先调用一个 opsForXxx 方法,未免有些麻烦。

  另外,还需要进行一个强制转换:Object -> Menu。说实话,我个人非常讨厌强制类型转换的代码写法(不知道有没有同感的。。。)

  所以我们可以写一个统一的分装类,提供一些常用的方法,将这些 opsForXxx 和 强转代码放在这些方法里面完成,一劳永逸。如:

public class RedisService {
    private StringRedisTemplate stringRedisTemplate;
    private RedisTemplate redisTemplate;

    public RedisService(StringRedisTemplate stringRedisTemplate, RedisTemplate redisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
        this.redisTemplate = redisTemplate;
    }
    // -------- key 相关命令 ------------------
    Long del(String... keys){
        return redisTemplate.delete(Arrays.asList(keys));
    }

    public Boolean exists(String key){
        return redisTemplate.hasKey(key);
    }

    public Boolean expire(String key, Long seconds){
        return redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
    }

    public Boolean expire(String key, Date date){
        return redisTemplate.expireAt(key, date);
    }

    public Set<String> keys(String pattern){
        return redisTemplate.keys(pattern);
    }

    public String randomKey(){
        return (String)redisTemplate.randomKey();
    }

    public void rename(String oldKey, String newKey){
        redisTemplate.rename(oldKey, newKey);
    }

    public DataType type(String key){
        return redisTemplate.type(key);
    }

    // -------- string 相关命令 -----------
    public void set(String key, Object object){
        if (object instanceof String){
            stringRedisTemplate.opsForValue().set(key, (String) object);
        }else {
            redisTemplate.opsForValue().set(key, object);
        }
    }

    public String get(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }

    public <T> T get(String key, Class<T> clazz){
        return (T)redisTemplate.opsForValue().get(key);
    }

    public <T> List<T> getList(String key, Class<T> clazz){
        return (List<T>)redisTemplate.opsForValue().get(key);
    }

    public <T> T getAndSet(String key, Object object, Class<T> clazz){
        return (T) redisTemplate.opsForValue().getAndSet(key, object);
    }

    public List<Object> multiGet(String... keys){
        return redisTemplate.opsForValue().multiGet(Arrays.asList(keys));
    }

    public void multiSet(Map<String, Object> map){
        redisTemplate.opsForValue().multiSet(map);
    }

    // ---------- list 相关命令 ------------------
}

  我这里只写了一部分,准备以后用到的时候再补充,毕竟这里只是一个例子。但是思路就是这么个思路。

 

5. 声明式缓存调用

  我们都知道,所有的缓存代码,都有一个统一的格式,用伪码可以这么表示:

data = getDataFromCache;
if (data is empty){
    data = getDataFromDB;
    if(data isNot empty){
        saveDataToCache;
    }
}

  在这个格式中,具体的业务相关的就只有   data = getDataFromDB;  这一行代码。其它的都是重复的。既然如此,为什么不将那些重复的代码封装起来,然后提供一个注解声明,当调用这行代码时,就自动执行缓存代码的逻辑呢?spring 已经帮我们做好了这件事。

  spring 提供了一套声明式缓存的接口,让我们只需要添加相关的依赖,配置缓存的第三方工具,然后在方法上添加注解,就可以使用注解。具体使用方法:

  5.1. 添加依赖

    文章开头已经给出了依赖的代码。

  5.2. 启用缓存

    在启动类添加   @EnableCaching  ,以启用缓存。

  5.3. 编写需要使用缓存的方法

@CacheConfig(cacheNames = "onezai-cloud-menuCache")
@Service
public class MenuService extends BaseService<MenuMapper, Menu> implements MenuIService {

    @Cacheable(key = "'getAll'")
    @Override
    public List<Menu> getAll() {
        return this.list();
    }
}

    在这里,我们在 MenuService 中编写一个 getAll 方法,该方法调用 list() 方法,无条件查询所有菜单(list() 是 mybatis-plus-generator 自动生成的,用过的朋友应该知道)。这里使用了两个注解:

     @CacheConfig :类级别的注解。指定该类所有方法上的缓存注解默认缓存名都是 cacheName (本例中即“onezai-cloud-menuCache”),缓存名可以理解为对一类缓存的一个集合(比如讲菜单相关数据的缓存放到一起);

     @Cacheable :方法级别注解。key 指定缓存的 key ,redis 是键值存储的,这里特就是指定键。不过该注解同样可以指定 cacheName,如果 CacheConfig 和 Cacheable 都指定了,则以颗粒度小的为准,即以 Cacheable 为准。需要注意的是,key 需要使用 spEL 表达式,所以也支持根据参数来组合生成 key;

    另外,还有一些其它注解: @CacheEvict   @CachePut   @Caching   。关于缓存注解的使用,请参考:

      spring cache 学习——缓存注解使用

 

6. 完

 

posted @ 2020-02-28 16:07  不爱刺猫的鱼  阅读(2071)  评论(0编辑  收藏  举报