@Cacheable总结
Redis的作用很多,缓存是其中之一,作为内存数据库,效率不言而喻,热点、高频搜索词汇基本都要进行缓存。Java使用redis是一件麻烦的事情,需要使用客户端API去操作,如Jedis 、lettuce 。Spring对Redis进行整合之后,使用就非常方便了,这里提一嘴,SpringBoot2.0之后将Redis的默认客户端由Jedis更换为lettuce,考虑到旧项目的升级并没有直接剔除Jedis。
1、依赖及配置文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
配置文件:application.properties
server.port=9003
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
#####Redis配置########
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
#spring.redis.password=123456
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接,默认值8
spring.redis.lettuce.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
2、@Cacheable简介
@Cacheable会将方法的返回值缓存到Redis中,第一次走数据库,后面都是直接走Redis查询数据。
看源码可以看出,该注解可以使用在接口、类及方法上,生效范围跟注解位置有关,方法上表示该方法开启缓存,类上,表示类的所有方法开启缓存。
使用很简单,在需要缓存的类或方法上添加注解@Cacheable,然后在启动类上添加@EnableCaching开启缓存。
@RestController
public class LogisticController {
@Autowired
private LogisticsMapper logisticsMapper;
@GetMapping("/test01/{id}")
@Cacheable(value = "logistic")
public Logistics test01(@PathVariable("id") Long id) {
Logistics logistics = logisticsMapper.selectById(id);
System.out.println("走数据库打印我");
return logistics;
}
}
访问http://localhost:9003/test01/15,第一次会走数据库,控制台打印“走数据库打印我”,再次访问走Redis,控制台不再输出。
接下来看看注解的属性:
/**
* 缓存块(可视化工具视角就是文件夹),必须要定义,可以为多个,如value = {"logistic","test"},cacheNames的别名
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 和value一样的
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
*Redis中的kry
*/
String key() default "";
/**
* 缓存key的生成策略
*/
String keyGenerator() default "";
/**
* 缓存管理器
*/
String cacheManager() default "";
/**
*缓存解析器
*/
String cacheResolver() default "";
/**
* 过滤条件,true代表会缓存,反之不会,可以写布尔表达式,默认空,也就是都会缓存
*/
String condition() default "";
/**
*否决缓存,此表达式是在方法之后计算的,默认空,从不否决,跟condition相反
*/
String unless() default "";
/**
*多线程查询时,是否同步调用,默认false
*/
boolean sync() default false;
}
如果我们只定义了缓存块,没有定义key,key会是什么呢?key默认是你的缓存块名称::SimpleKey [],数组里面是方法的参数值;比如
@Cacheable(value = "logistic")
public Logistics test02( Long id,String code) {
Logistics logistics = logisticsMapper.selectById(id);
System.out.println("走数据库打印我");
return logistics;
}
kety就是 logistic::SimpleKey [15,E012454],但是这样是有问题的,假如参数值一样或者没有参数,就会造成key被覆盖,出现脏数据,因此key必须要自定义,比如使用方法名拼接参数,如下:
@Cacheable(value = "logistic",key = "'test02'+#id+#code")
public Logistics test02(@PathVariable("id") Long id,@PathVariable("code")String code) {
Logistics logistics = logisticsMapper.selectById(id);
System.out.println("走数据库打印我");
return logistics;
}
id=15,code=E012454,key就变为 logistic::test0215E012454
注意:获取传入的参数需要前面加#,字符拼接使用单引号。
如果还是不满足要求,可以使用配置类解决,新建配置类继承CachingConfigurerSupport,重写相关方法,进行自定义:
@Configuration
@Slf4j
public class RedisConfig extends CachingConfigurerSupport {
/**
* key生成规则,使用全限定类名+方法名+参数值作为key,注解上如果有定义key规则,这里就不会生效
*
* @return
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
public RedisConfig() {
super();
}
/**
* 缓存管理器,可以甚至一些redis的配置,但是一般直接在properties文件设置了
*
* @return
*/
@Override
public CacheManager cacheManager() {
return super.cacheManager();
}
@Override
public CacheResolver cacheResolver() {
return super.cacheResolver();
}
/**
* Redis数据操作异常处理,进行自定义配置,如果redis挂了,会绕过redis直接走数据库,反之则会抛出异常
*
* @return
*/
@Bean
@Override
public CacheErrorHandler errorHandler() {
CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.error("从redis推送数据异常,redis可能挂了");
}
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.error("从redis获取数据异常,redis可能挂了");
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
}
};
return cacheErrorHandler;
}
说明:如果Redis宕机了,会直接抛出异常,并不会像想象中的那样,Reds宕机就走数据库拿数据,因此我们需要重写errorHandler()方法,进行自定义,只要自定义了,即使Redis宕机也会转从数据库捞数据,不至于影响主业务流程。这里有个时间需要设置一下,spring.redis.timeout=1000,就是Redis连接超时时间,默认1分钟,也就是说会等待一分钟,确认连不上Redis才会走数据库,时间太长设置1s就i行,这里什么都不做就打个日志。停掉Redis访问,控制台日志如下:
3、缓存不生效
场景:同一个类中A、B方法都加缓存注解,但是在A中去调用B方法,A成功写入缓存,但是B却没有
原因:这个其实和Spring的声明式事务一样的情况,是因为他们都是基于AOP的,AOP使用的是代理类,也就是说AOP的最小粒度是Class,在A方法中调用B相当于this.B(),B方法并不是通过代理类进行调用的,因此不会经过切面,所有跟AOP相关的都是一样的。
解决:要么在类中注入自身,再去调用,或者从Spring容器的上下文中获取该类之后再去调用B方法。