• 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. 完

  • 相关阅读:
    Python学习
    我的计算机网络复习笔记(第一章)
    理解DES算法
    彻底理解RSA加密算法
    扩展欧几里得算法求模的乘法逆元
    python的deque(双向)队列详解
    对于暴力枚举的一些优化方法的题解
    python中的多(liu)元(mang)交换 ,赋值
    python定义函数后跟->的意义
    直接暴力做分糖问题
  • 原文地址:https://www.cnblogs.com/coding-one/p/12376607.html
Copyright © 2020-2023  润新知