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 。关于缓存注解的使用,请参考:
6. 完