对于热点数据,一般存储在缓存中,当需要时直接从缓存中获取,在缓存找不到的时候才访问数据库。访问缓存和向缓存中插入数据,是一个统一标准的动作,代码都是一样的,如果在每个方法里面都写这么一段访问缓存的代码,那会造成大量代码的冗余,spring也想到了这一点,它为我们提供了一些注解,对缓存数据的存取只需在方法上添加这些注解即可,无需再代码里面编写繁琐重复的代码。
下面先介绍一个例子,然后介绍spring缓存数据背后的原理。
文章目录
- 一、使用spring缓存
- 二、关键注解介绍
- 1、@Cacheable
- 2、@CachePut
- 3、@CacheEvict
- 三、实现原理
- 参考文章
一、使用spring缓存
使用spring缓存需要在代码中完成以下几步:
1、在pom文件中添加cache依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2、在主程序中添加注解@EnableCaching
以开启基于注解的缓存。
@EnableCaching
public class Main {
public static void main(String[] args) throws Exception {
// 创建spring容器
ApplicationContext context =SpringApplication.run(Main.class, args);
}
}
3、在需要缓存数据的方法上添加@Cacheable、@CachePut
等注解。为了简单,例子中没有真实访问数据库。
@Component("cacheTest")
public class CacheTest {
//先检查是否有缓存,如果有则不执行方法,如果没有则执行方法并将方法返回值放入缓存
@Cacheable(cacheNames="test")
public User testCaching(String name){
System.out.println("没有找到数据访问数据库");
User user=new User();
user.setAge(18);
user.setName(name);
return user;
}
//将方法返回值放入到缓存中
@CachePut(cacheNames="test")
public User testPutCaching(String name){
System.out.println("数据放入缓存");
User user=new User();
user.setAge(19);
user.setName(name);
return user;
}
//删除缓存
@CacheEvict(cacheNames="test")
public void testEvictCaching(String name){
System.out.println("删除缓存");
}
}
调用CacheTest方法的流程如下:
CacheTest cache=(CacheTest)context.getBean("cacheTest");
//将数据放入缓存
System.out.println(cache.testPutCaching("张三"));
//数据已经在缓存,不访问数据库
System.out.println(cache.testCaching("张三"));
//删除缓存
cache.testEvictCaching("张三");
//数据不在缓存中,访问数据库
System.out.println(cache.testCaching("张三"));
//数据已缓存中,不访问数据库
System.out.println(cache.testCaching("张三"));
//以“张三”为key的数据已缓存中,但是“李四”不在
System.out.println(cache.testCaching("李四"));
上面代码的执行结果如下:
数据放入缓存
name=张三;age=19
name=张三;age=19
删除缓存
没有找到数据访问数据库
name=张三;age=18
name=张三;age=18
没有找到数据访问数据库
name=李四;age=18
二、关键注解介绍
1、@Cacheable
@Cacheable用于类或方法上,在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值,不再执行目标方法;没有则执行目标方法,并将方法的返回值作为value,并以键值对的形式存入缓存。
关键属性如下:
- cacheNames/value:缓存名,相当于命名空间,String类型的数组,可以指定多个缓存名,不同命名空间下,可以有相同的key。执行方法前,按顺序遍历每个命名空间,如果其中一个缓存空间中有需要的key,那么返回该空间下的key对应的value,如果都没有,则执行方法,执行后,将键值对放入到每个缓存空间下。
- key:缓存键,默认是方法的所有入参值,也可以使用SpEL表达式指定。
- keyGenerator:键产生器,用于产生键,该属性与key是互斥的,如果在spring容器中有多个产生器,可以使用该属性指定;如果没有指定key,spring也会使用产生器产生key,前提是容器中有产生器。
- cacheManager:指定缓存管理器,在一个容器中可以同时存在多个管理器,属性值是缓存管理器在容器中的名字;
- cacheResolver:解析器,该属性与cacheManager是互斥的,只能指定一个;
- condition:在激活注解功能前,进行condition验证,如果condition结果为true,则表明验证通过,缓存注解生效;否则缓存注解不生效。使用SpEL表达式。默认是true。
- unless:是否令注解(在方法执行后的功能)不生效;若unless的结果为true,则(方法执行后的功能)不生效;若unless的结果为false,则(方法执行后的)功能生效。
- sync:表示强制同步执行。如果多个线程试图查找同一个键的value,那么Cache会对key进行加锁,以同步的方式来进行目标方法的调用,同步的好处是:后一个线程会读取到前一个线程的缓存数据,不用再调用目标方法了。默认为false,表示不用同步。是否能够同步还依赖于底层Cache的实现,有些Cache是不支持同步的,也就是即使设置sync=true也是以非同步的方式执行。
2、@CachePut
@CachePut用于类或方法上,在执行完目标方法后,将方法的返回值作为value,以键值对的形式存入缓存。
该注解的属性与@Cacheable相同。
3、@CacheEvict
@CacheEvict用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话)。
下面只介绍与@Cacheable不相同的属性:
- allEntries:表示是否清除指定命名空间中的所有数据,默认为false。
- beforeInvocation:是否在目标方法执行前删除缓存数据。 默认为false,即目标方法执行完毕后删除数据。
注解的属性中使用了SpEL表达式,下面介绍一下SpEL表达式的规则:
名字 | 位置 | 描述 | 例子 |
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表 | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字. 可以直接#参数名,也可以使用#p0或#a0 的形式,0代表参数的索引 | #iban、#a0 、#p0 |
result | evaluation context | 方法执行后的返回值 | #result |
三、实现原理
spring缓存里面涉及到几个关键类:
- CacheManager:缓存管理器,一般的spring容器中只能有一个管理器存在,如果定义多个,启动时会抛出
NoUniqueBeanDefinitionException
异常,如果定义了多个管理器,需要使用@Primary
指定一个主管理器,如果不设置注解的cacheManager属性值,默认都是使用该主管理器,缓存管理器中持有所有由该管理器管理的Cache对象; - CacheResolver:缓存解析器,与CacheManager一一对应,作用是查找Cache对象;
- Cache:存储缓存数据,类似于一个缓存的存储器,第二节介绍的三个注解的属性cacheNames指的便是Cache的名字,key和value存储在Cache中;
- CacheOperation:方法上的每个注解都被解析为一个CacheOperation对象,并将方法与CacheOperation对象集合的对应关系保存到Map对象中,CacheOperation有三个子类:
CacheableOperation、CacheEvictOperation、CachePutOperation
,分别对应三个注解,注解的属性值保存在三个子类对象中,子类对象里面没有具体的处理逻辑,只是用来存储属性值,spring执行时,通过Map对象找到CacheOperation对象集合,然后判断某个CacheOperation对象是否有,如果有则执行对应的逻辑,如果没有则跳过;
先介绍一下注解@EnableCaching
的作用。
注解@EnableCaching
加载了一个配置类ProxyCachingConfiguration,该配置类又引入了一个非常关键的类:CacheInterceptor
缓存拦截器。该拦截器负责拦截添加了注解的方法(拦截通过AOP实现),如果没被该拦截器拦截,那么无法使用spring缓存。CacheInterceptor
拦截了方法之后,首先获得该方法的CacheOperation对象集合,然后遍历该集合,根据CacheOperation的属性值,从容器中找到如下对象:KeyGenerator、CacheResolver、CacheManager
,解决了这三个对象之后,就可以根据cacheNames找到对应的Cache对象,这样CacheOperation、KeyGenerator、CacheResolver、CacheManager、Cache之间形成了一一对应关系。
接下来,CacheInterceptor
检查CacheOperation对象集合是否有CacheEvictOperation
对象,其实也就是检查是否有@CacheEvict注解,如果有,则根据condition和beforeInvocation
属性的配置来决定是否删除缓存;然后判断是否有CacheableOperation
对象,如果有则先检查Cache对象中是否有对应缓存数据,如果有多个Cache对象,则进行遍历,如果从缓存中找到了缓存数据,则不再调用方法,如果没有找到则调用方法;之后判断是否有CachePutOperation
或者CacheableOperation
对象,如果有将缓存存储到Cache中,如果有多个Cache,则每个都存储;最后是再次判断是否有CacheEvictOperation
对象,如果有则根据condition
属性的配置来决定是否删除缓存。执行完上述逻辑之后,将方法返回值返回给调用方,处理结束。
CacheInterceptor
处理过程中查找KeyGenerator、CacheResolver、CacheManager三个对象,如果配置了cacheResolver或者cacheManager属性,那么根据属性值查找CacheResolver和CacheManager对象,如果没有配置,那么使用容器中默认的CacheResolver和CacheManager对象。KeyGenerator也是一样,如果没有配置属性keyGenerator,spring会创建一个SimpleKeyGenerator对象作为key产生器。SimpleKeyGenerator会将方法所有入参作为key,处理规则比较简单:
- 如果方法没有入参,则创建一个空的SimpleyKey对象作为key;
- 如果方法有一个入参,那么该入参就是key;
- 如果方法有多个入参,那么首先创建一个SimpleyKey对象,然后将方法入参组成数组作为SimpleyKey的属性,SimpleyKey对象作为key返回。