springBoot版本为2.0.4
一、基于代码的方式
方式一:基于redisTemplate
步骤:
1、引入redis的starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、springBoot配置文件中配置redis,例如:
############## redis的配置 ##############################
redis.host=localhost
redis.port=6379
redis.auth=123456
#连接和读写redis的超时时间
redis.timeOut=3000
#redis默认库号,0号库
redis.baseId=0
3、在需要用到的地方,自动注入RedisTemplate<Object,Object> redisTemplate(操作所有类型的数据) 或者StringRedisTemplate stringRedisTemplate(专门操作String类型),调用他的opsForxxx()方法,进而来调用redis的api即可。
@Autowired
private RedisTemplate<Object,Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test01() {
Employee employee = (Employee)redisTemplate.opsForValue().get("employee::1");
System.out.println(employee);
}
小问题:
例如这样:用redisTemplate将一个对象放到redis中,redis中看着会乱码。
Employee employee = employeeMapper.getEmployeeById(1);
redisTemplate.opsForValue().set("emp1",employee);
原因是:由于redisTemplate默认的对象序列化方式为jdk自带的序列化方式,这样做便于redisTemplate进行反序列化,便于还原成原来的对象,其实不算是乱码。
若我们希望它能使以json字符串的方式存储到redis中,应该怎么做呢?
解决方法:
1、将对象转成json字符串存储。
2、改变RedisTemplate默认的序列化方式为json序列化。
方式1很简单先不说,这里我们说一下方式2:
根据上面分析的原因,只要我们改变一下redisTemplate的默认的对象序列化方式即可。springBoot允许我们这样做,在redis的自动配置类RedisAutoConfiguration中,往IOC容器中装配RedisTemplate时,有这样一个注解@ConditionalOnMissingBean,它的意思是当IOC容器中没有RedisTemplate时,才会使用自动装配的RedisTemplate,若有客户自定义的RedisTemplate,就是用自定义的RedisTemplate,所以我们只需要在配置类里面,配置一个RedisTemplate,然后通过@Bean注解注入到IOC容器中就行了。
配置一个RedisTemplate<Object,Object>覆盖默认的:
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//设置成jackson序列化的方式,因为默认的jdk序列化对中文会乱码
//或者每次都手动的序列化成json字符串和反序列化
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
这样往redis中存放数据是json的没问题,但是这样
Employee emp = (Employee)redisTemplate.opsForValue().get("emp1");
System.out.println(emp);
取数据时,就会报ClassCastException,原因就是配置的RedisTemplate<Object,Object>,value是Object类型,一个json数据反序列化成Object时,会用LinkedHashMap来存储,但是你LinkedHashMap强转成Employee类型,肯定会报错。解决办法也很简单,若要转换成json存储,就必须给每个要转换的类型配置一个独立的RedisTemplate<Object,beanType>才行。
/*----------操作Employee的RedisTemplate配置Jackson序列化方式*/
@Bean
public RedisTemplate<String,Employee> empRedisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<String,Employee> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Employee.class);
//设置成jackson序列化的方式,默认的jdk序列化对中文会乱码
//或者每次都手动的序列化成json字符串和反序列化
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
/*----------操作Department的RedisTemplate配置Jackson序列化方式*/
@Bean
public RedisTemplate<String,Department> deptRedisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<String,Department> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer<Department> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Department.class);
//设置成jackson序列化的方式,默认的jdk序列化对中文会乱码
//或者每次都手动的序列化成json字符串和反序列化
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
...其他类型的RedisTemplate
然后注入刚才配置的RedisTemplate即可。
@Autowired
private RedisTemplate<String,Employee> redisTemplate;
@Autowired
private RedisTemplate<String,Department> deptRedisTemplate;
同理在用redis实现缓存注解时,也要为每个要存储的bean配置一个独立的xxxCacheManager,否则也会报类型转换异常。你也许会觉得这样太麻烦了,没办法,使用别人的框架还这么多要求,就得按照人家的方法来做。
总之:
1)若想省事就注入RedisTemplate<Object,Object>直接操作即可,什么都不用配置,但是要容许“视觉上的乱码”。
2)若想转换成json存储,就老老实实的给每个类型配置RedisTemplate吧。
或者使用现成的 redisUtils工具类,也很简单方便。
方式二:基于jedis
本人更倾向于使用jedis来操作redis,具体原因后面会说到。
具体的可以这么做:
利用redis的Java客户端jedis,自己封装一个redis操作工具类(基于jedis和jedisPool),里面单例模式一个jedisPool,获取和释放连接的方法,以及封装的api方法。
如果你要说自己封装比较麻烦,没有关系,java这么丰富的周边生态,还怕找不到一个小小的工具类吗?如果不嫌弃的话,本人也封装了一个基于jedis的工具类,基本涵盖了1V5中的所有api的操作方法,并配有详细的注释,目前也在持续更新中,欢迎来扰,一个超全的基于jedis的redis工具类: https://blog.csdn.net/qq_40925189/article/details/109585667
关于该工具类的一些方法被调用:
RedisUtils.setex(token,Constant.Redis.tokenValidTime, jsonUser);
RedisUtils.hget(Constant.Redis.loginUserHashMap, String.valueOf(userId));
RedisUtils.hset(Constant.Redis.loginUserHashMap,String.valueOf(userId),token);
RedisUtils.del(token);
RedisUtils.hdel(Constant.Redis.loginUserHashMap,String.valueOf(id));
二、基于注解的方式
因为我们是使用spring的缓存注解来操作redis,而这些spring的缓存注解又是基于spring的缓存抽象,所以先知道了spring的缓存抽象是怎么样的,才能对这些缓存注解的使用得心应手。
1、spring中的缓存抽象
javaEE规范中的JSR-107规范,定义了在java中使用cache的一些规范,它定义了很多复杂的缓存组件概念和接口,并没有提供实现。由于这些复杂的概念和接口实现起来比较麻烦,所以spring从3.1开始,在JSR-107规范的基础上将缓存抽象成了简单的两部分org.springframework.cache.Cache和org.springframework.cache.CacheManager,来统一不同的缓存技术,并支持使用JCache(JSR-107)注解简化我们开发。
在spring中cache和cacheManager的定义和关系大致长这样:
如图我们可以看到,缓存是建立在应用程序和数据库之间的一层。可以有多个CacheManager,由CacheManager管理着一个个的cache。
CacheManager:缓存管理器,管理各种缓存(Cache)组件,比如默认的ConcurrentMapCacheManager(使用的是SimpleCacheConfiguration配置类、cache组件为ConcurrentMapCache、将数据保存到一个ConcurrentHashMap中)、RedisCacheManager(使用的是RedisCacheConfiguration配置类,cache组件为RedisCache,数据存储到redis中)等;
Cache:缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache(默认)等。cache中包含许许多多的key-value对,被称为entry或element。缓存注解中的cacheNames属性,就表示某个cache的名字,必须是唯一的。key就表示cache里面某个entry的唯一标识key。
2、基于缓存注解操作redis
值得一提的是:spring缓存注解的实现是用的拦截器(interceptor),所以若是想统计每个controller方法的执行时间,需要使用更早加载的过滤器(filter)或者使用@order(1)注解指定多个拦截器的加载顺序。
步骤:很简单
1、引入spring缓存抽象的依赖和redis的starter的依赖
<!--引入spring的缓存抽象-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--redis的starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、在启动类或任何配置类上添加@EnableCaching(开启基于注解的缓存)
3、针对每个bean类型,配置对应的cacheManager。(为什么要给每一个bean类型配置对应的cacheManager,在下面的小问题中会说到。)
4、在controller或serviceImpl的方法上加上缓存注解即可(将方法的返回值放入redis中)。
一个简单的小栗子:
@Cacheable(cacheNames = "employee",key = "#id")
@GetMapping("/emp/{id}")
public Employee getEmpById(@PathVariable("id") Integer id){
Employee employee = employeeMapper.getEmployeeById(id);
return employee;
}
//@CachePut会先到缓存中查询一下,有就更新,没有就添加。
@CachePut(cacheNames = "employee",key = "#employee.id")
@GetMapping("/addEmp")
public Employee addEmp(Employee employee){
employeeMapper.addEmployee(employee);
return employee;
}
@CachePut(cacheNames = "employee",key = "#employee.id")
public Employee updateEmployee(Employee employee){
employeeMapper.updateEmployee(employee);
return employee;
}
@CacheEvict(cacheNames = "employee",key = "#id")
//@CacheEvict(cacheNames = "employee",allEntries = true)
@GetMapping("/deleteEmp")
public String deleteEmployee(Integer id){
System.out.println("删除id为"+id+"的员工。");
return "删除成功!";
}
//@Caching组合多个注解,如此时添加员工后,往缓存中以key为id、username、
//email,value为employee放了三个k-v对,此时即使没有根据email查询的方法,
//因为缓存中有,也可以从缓存中取出。如果缓存中没有去查数据库,因为是根据Integer
//类型的数据去查询的,所以此时数据会封装不上
@Caching(
put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
}
)
public Employee addEmp(Employee employee){
return employeeMapper.addEmployee(employee);
}
为什么导入redis的starter,就能让缓存注解的实现变成redis呢?
切换的原理:
默认情况下,spring对缓存注解的缓存实现是SimpleCache,即cacheManager是ConcurrentMapCacheManager,cache组件为ConcurrentMapCache,我们只要将注解的缓存实现变成redis就行了,即cacheManager为RedisCacheManager,cache组件为RedisCache,怎么做呢?
在org.springframework.boot.autoconfigure.cache包下有个CacheConfigurations类,定义了各种cacheConfiguration的加载顺序,每个cacheConfiguration上面都有一个@ConditionalOnMissingBean({CacheManager.class})注解(即保证了先加载的cacheConfiguration若往IOC容器中装配一个CacheManager类,其他的cacheConfiguration都不会生效),而RedisCacheConfiguration(redis支持缓存注解时的自动配置类)的加载顺序在默认的SimpleCacheConfiguration之前,所以说若能保证RedisCacheConfiguration生效,就能将对缓存注解的支持变成redis。RedisCacheConfiguration类上面有一个注解@ConditionalOnClass({RedisConnectionFactory.class}),即若IOC容器中有RedisConnectionFactory这个类,就能使RedisCacheConfiguration生效,当这个RedisCacheConfiguration生效后会自动往IOC容器装配一个RedisCacheManager,RedisCacheManager又会自动生成RedisCache,所以只要IOC容器中有RedisConnectionFactory这个类,就能成功的将缓存注解的支持变成redis,而恰好RedisConnectionFactory这个类就在我们导入的spring-boot-starter-data-redis里面。
结论:只要导入了spring-boot-starter-data-redis这个依赖,就能让redis来做spring缓存抽象的支持,就能通过注解实现缓存数据到redis。
小问题
同上面使用redisTemplate时一样,RedisCacheConfiguration默认的对象序列化方式不是json格式的而是二进制的,也是不影响反序列化。但若想用json进行序列化,需要进行如下配置:
springBoot2.x
方式1(推荐)、为每一个要缓存的对象所属的类,都设置一个缓存管理器,并指定为json序列化。(这么做的原因,猜想可能是:利用json工具手动序列化和反序列化时,我们知道要反序列化后的对象的类型,但现在基于注解自动反序列化时,可能不确定要反序列化的对象类型,所以需要每个类型都指定一下,否则就会报类型转换异常)。
方式2、或者value的序列化方式用这个 new GenericJackson2JsonRedisSerializer(),这样只写一个xxxCacheManager就行,但是json数据中会带着"@class": "com.test.springboot.bean.Employee"这个信息,只能用原来注解序列化的方式反序列化,不能用工具类手动的反序列化,通用性不强。
关于方式2可以这样做:
@Bean
public RedisCacheManager generalCacheManager(RedisConnectionFactory connectionFactory){
Jackson2JsonRedisSerializer<String> stringSerializer = new Jackson2JsonRedisSerializer<>(String.class);
//配置redis
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofDays(1))//设置缓存有效期为一天
.computePrefixWith(cacheName -> cacheName.concat(":"))//设置缓存key的前缀为cacheName:key的形式
.disableCachingNullValues();
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
关于方式1这样做:
/**
* 配置Employee类型的缓存管理器
* */
@Primary //当有多个缓存管理器时,必须要指定一个是主要的
@Bean
public RedisCacheManager empCacheManager(RedisConnectionFactory connectionFactory){
Jackson2JsonRedisSerializer<String> stringSerializer = new Jackson2JsonRedisSerializer<>(String.class);
Jackson2JsonRedisSerializer<Employee> employeeSerializer = new Jackson2JsonRedisSerializer<>(Employee.class);
//RedisCacheConfiguration配置序列化方式
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(employeeSerializer))
.prefixKeysWith("aaa") //缓存key的前缀为aaa1的形式
.disableCachingNullValues();
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
/**
* 配置Department类型的缓存管理器
*/
//@Bean
public RedisCacheManager deptCacheManager(RedisConnectionFactory connectionFactory){
Jackson2JsonRedisSerializer<String> stringSerializer = new Jackson2JsonRedisSerializer<>(String.class);
Jackson2JsonRedisSerializer<Department> deptSerializer = new Jackson2JsonRedisSerializer<>(Department.class);
//默认1
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(deptSerializer))
.entryTtl(Duration.ofDays(1))//设置缓存有效期为一天
.computePrefixWith(cacheName -> cacheName.concat(":"))//设置缓存key的前缀为cacheName:key的形式
.disableCachingNullValues();
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
//key的生成策略,可以在@Cacheable注解中使用该策略
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString()+"]";
}
};
然后就可以在缓存注解中使用keyGenerator和cacheManager指定要使用哪个key的生成规则、和定义的cacheManager了。
在springBoot1.x时针对方式1可以这样做:
RedisCacheManager的构建是基于RedisTemplate,而RedisTemplate的默认序列化方式也是基于JDK的,看着像乱码,所以我们要改成使用json序列化的方式。
@Bean
public RedisTemplate<Object, Employee> empRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
template.setDefaultSerializer(ser);
return template;
}
@Primary //将某个缓存管理器作为默认的
@Bean
public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> empRedisTemplate){
RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate);
//使用前缀,默认会将CacheName作为key的前缀
cacheManager.setUsePrefix(true);
//其他配置
return cacheManager;
}
关于缓存注解参数的详解:
@Cacheable、@CachePut、@CacheEvict、的常用参数
1、cacheNames/value:指定使用一个或多个cache,必须指定一个。
如:cacheNames="emp" / value={"emp","cache2","cache3"}
2、key:某个cache中某个缓存的唯一标识。当参数为一个时,默认为这个参数名 当参数为多个时,key为参数的组合。指定时需要按照spEL表达式的方式指定。
如:key="#id" / key="p0"
3、keyGenerator:指定缓存key的生成器。
4、cacheManager:指定缓存对应的缓存管理器,否则使用@Primary标注的缓存管理器。
3、condition:条件判断,根据condition指定的条件判断为true时才会被缓存/清除。
如:@Cacheable(cacheNames="emp",key="id",condition="#username.length()>3")
4、allEntries(@CacheEvict的属性):默认为false,为true时,删除指定cache中的全部key-value对,在方法执行完毕后删除缓存。
5、beforeInvocation(@CacheEvict的属性):默认为false,为true时则在方法执行之前就清除缓存。
6、unless(@CachePut/@Cacheable的属性):根据条件判断,和confition相反,为false时才进行缓存。
cache中可用的spEL表达式
1、argumentname:方法参数的名字,可以直接 #参数名 ,也可以使用 #p0 或 #a0 的形式,0代表参数的索引;如:#id、 #a0 、 #p0
2、args:被调用方法的参数列表。如: key="#root.args[0]" 代表使用第一个参数。
3、result:代表方法返回的结果(仅当方法一定会被执行时) 如: key="#result.id"
虽然本文完成了用缓存注解来操作redis,但不推荐用缓存注解来做,还是推荐编码的方式来做,因为比较可控。关于缓存注解,可以使用一些本地缓存框架来实现,从而可以做一个简单的二级缓存功能。
本文springBoot整合redis到此就算结束了,有什么疑问和建议可以在评论区提出,欢迎指正。
上一篇:7、redis的主从复制
下一篇: