我们知道绝⼤多数的⽹站
/
系统,最先遇到的⼀个性能瓶颈就是数据库,使⽤缓存做数据库的前置缓存,可以
⾮常有效地降低数据库的压⼒,从⽽提升整个系统的响应效率和并发量。
以往使⽤缓存时,通常创建好缓存⼯具类,使⽤时将对应的⼯具类注⼊,操作⼯具类在前端处理缓存的逻
辑。其实这种⽅式是低效的,⼤部分使⽤缓存的场景是基于数据库的缓存,这类缓存场景的逻辑往往是:如
果缓存中存在数据,就从缓存中读取,如果缓存中不存在数据或者数据失效,就再从数据库中读取。
为了实现这样的逻辑,往往需要在业务代码中写很多的逻辑判断,那么有没有通⽤的代码来实现这样的逻辑
呢?其实有,按照这个逻辑我们可以写⼀个⼯具类来实现,每次需要这样判断逻辑时调⽤⼯具类中的⽅法即
可,还有没有更优雅的使⽤⽅式呢?答案是肯定的,如果我们把这种固定的逻辑使⽤
Java
注解来实现,每
次需要使⽤时只需要在对应的⽅法或者类上写上注解即可。
Spring
也看到了这样的使⽤场景,于是有了
注释驱动的
Spring Cache
。它的原理是
Spring Cache
利⽤了
Spring AOP
的动态代理技术,在项⽬启动的时候动态⽣成它的代理类,在代理类中实现了对应的逻辑。
Spring Cache
是在
Spring 3.1
中引⼊的基于注释(
Annotation
)的缓存(
Cache
)技术,它本质上不是⼀个
具体的缓存实现⽅案,⽽是⼀个对缓存使⽤的抽象,通过在既有代码中添加少量它定义的各种
Annotation
,
即能够达到缓存⽅法的返回对象的效果。
Spring
的缓存技术还具备相当的灵活性
,不仅能够使⽤
SpEL
(
Spring Expression Language
)来定义缓存
的
key
和各种
condition
,还提供了开箱即⽤的缓存临时存储⽅案,也⽀持和主流的专业缓存如
EHCache
集
成。
SpEL
(
Spring Expression Language
)是⼀个⽀持运⾏时查询和操作对象图的强⼤的表达式语⾔,其
语法类似于统⼀
EL
,但提供了额外特性,
显式⽅法调⽤和基本字符串模板函数
。
其特点总结如下:
通过少量的配置
Annotation
注释即可使得既有代码⽀持缓存;
- ⽀持开箱即⽤ Out-Of-The-Box,即不⽤安装和部署额外第三⽅组件即可使⽤缓存;
- ⽀持 Spring Express Language,能使⽤对象的任何属性或者⽅法来定义缓存的 key 和 condition;
- ⽀持 AspectJ,并通过其实现任何⽅法的缓存⽀持;
- ⽀持⾃定义 key 和⾃定义缓存管理者,具有相当的灵活性和扩展性。
Spring Boot 中 Cache 的使⽤
Spring Boot
提供了⾮常简单的解决⽅案,这⾥给⼤家演示最核⼼的三个注解:
@Cacheable
、
@CacheEvict
、
@CachePut
。
spring-boot-starter-cache
是
Spring Boot
体系内提供使⽤
Spring Cache
的
Starter
包。
在开始使⽤这三个注解之前,来介绍⼀个新的组件
spring-boot-starter-cache
。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring-boot-starter-cache
是
Spring Boot
提供缓存⽀持的
starter
包,其会进⾏缓存的⾃动化配置和识别,
Spring Boot
为
Redis
⾃动配置了
RedisCacheConfifiguration
等信息,
spring-boot-starter-cache
中的注解也
主要是使⽤了
Spring Cache
提供的⽀持。
@Cacheable
@Cacheable
⽤来声明⽅法是可缓存的,将结果存储到缓存中以便后续使⽤相同参数调⽤时不需执⾏实际的
⽅法,直接从缓存中取值。
@Cacheable
可以标记在⼀个⽅法上,也可以标记在⼀个类上。当标记在⼀个⽅
法上时表示该⽅法是⽀持缓存的,当标记在⼀个类上时则表示该类所有的⽅法都是⽀持缓存的。
我们先来⼀个最简单的例⼦体验⼀下:
@RequestMapping("/hello")
@Cacheable(value="helloCache")
public String hello(String name) {
System.out.println("没有⾛缓存!");
return "hello "+name;
}
来测试⼀下,启动项⽬后访问⽹址
http://localhost:8080/hello?name=neo
,输出:没有⾛缓存!,再次访问
⽹址
http://localhost:8080/hello?name=neo
,输出栏没有变化,说明这次没有⾛
hello()
这个⽅法,内容直接
由缓存返回。
@Cacheable(value="helloCache")
这个注释的意思是,当调⽤这个⽅法时,会从⼀个名叫
helloCache
的缓
存中查询,如果没有,则执⾏实际的⽅法(也可是查询数据库),并将执⾏的结果存⼊缓存中,否则返回缓
存中的对象。这⾥的缓存中的
key
就是参数
name
,
value
就是返回的
String
值。
@Cacheable
⽀持如下⼏个参数。
- value:缓存的名称。
- key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写;如果不指定,则缺省按照⽅法的所有 参数进⾏组合。
- condition:触发条件,只有满⾜条件的情况才会加⼊缓存,默认为空,既表示全部都加⼊缓存,⽀持 SpEL。
我们把上⾯的⽅法稍微改成这样:
@RequestMapping("/condition")
@Cacheable(value="condition",condition="#name.length() <= 4")
public String condition(String name) {
System.out.println("没有⾛缓存!");
return "hello "+name;
}
启动后在浏览器中输⼊⽹址
http://localhost:8080/condition?name=neo
,第⼀次输出栏输出:没有⾛缓存!
再次执⾏⽆输出,表明已经⾛缓存。在浏览器中输⼊⽹址
http://localhost:8080/condition?name=ityouknow
,
浏览器执⾏多次仍然⼀直输出:没有⾛缓存!说明条件
condition
⽣效。
结合数据库的使⽤来做测试:
@RequestMapping("/getUsers")
@Cacheable(value="usersCache",key="#nickname",condition="#nickname.length() >= 6")
public List<User> getUsers(String nickname) {
List<User> users=userRepository.findByNickname(nickname);
System.out.println("执⾏了数据库操作");
return users;
}
启动后在浏览器中输⼊⽹址
http://localhost:8080/getUsers?nickname=neo
。
输出栏输出:
Hibernate: select user0_.id as id1_0_, user0_.email as email2_0_, user0_.nickname
as nickname3_0_, user0_.pass_word as pass_wor4_0_, user0_.reg_time as reg_time5_0_
, user0_.user_name as user_nam6_0_ from user user0_ where user0_.nickname=?
执⾏了数据库操作
多次执⾏,仍然输出上⾯的结果,说明每次请求都执⾏了数据库操作,再输⼊
http://localhost:8080/getUsers?nickname=ityoukonw
进⾏测试。只有第⼀次返回了上⾯的内容,再次执⾏输
出栏没有变化,说明后⾯的请求都已经从缓存中拿取了数据。
最后总结⼀下:当执⾏到⼀个被
@Cacheable
注解的⽅法时,
Spring
⾸先检查
condition
条件是否满⾜,如
果不满⾜,执⾏⽅法,返回;如果满⾜,在缓存空间中查找使⽤
key
存储的对象,如果找到,将找到的结果
返回,如果没有找到执⾏⽅法,将⽅法的返回值以
key-value
对象的⽅式存⼊缓存中,然后⽅法返回。
需要注意的是当⼀个⽀持缓存的⽅法在对象内部被调⽤时是不会触发缓存功能的。
@CachePut
项⽬运⾏中会对数据库的信息进⾏更新,如果仍然使⽤
@Cacheable
就会导致数据库的信息和缓存的信息不
⼀致。在以往的项⽬中,我们⼀般更新完数据库后,再⼿动删除掉
Redis
中对应的缓存,以保证数据的⼀致
性。
Spring
提供了另外的⼀种解决⽅案,可以让我们以优雅的⽅式去更新缓存。
GitChat
与
@Cacheable
不同的是使⽤
@CachePut
标注的⽅法在执⾏前,不会去检查缓存中是否存在之前执⾏过
的结果,⽽是每次都会执⾏该⽅法,并将执⾏结果以键值对的形式存⼊指定的缓存中。
以上⾯的⽅法为例,我们再来做⼀个测试:
@RequestMapping("/getPutUsers")
@CachePut(value="usersCache",key="#nickname")
public List<User> getPutUsers(String nickname) {
List<User> users=userRepository.findByNickname(nickname);
System.out.println("执⾏了数据库操作");
return users;
}
我们新增⼀个
getPutUsers
⽅法,
value
、
key
设置和
getUsers
⽅法保持⼀致,使⽤
@CachePut
。同时⼿动
在数据库插⼊⼀条
nikename
为
ityouknow
的⽤户数据。
INSERT INTO `user` VALUES ('1', 'ityouknow@126.com', 'ityouknow', '123456', '2018'
, 'keepSmile');
在浏览器中输⼊⽹址
http://localhost:8080/getUsers?nickname=ityouknow
,并没有返回⽤户昵称为
ityouknow
的⽤户信息,再次输⼊⽹址
http://localhost:8080/getPutUsers?nickname=ityouknow
可以查看到
此⽤户的信息,再次输⼊⽹址
http://localhost:8080/getUsers?nickname=ityouknow
就可以看到⽤户昵称为
ityouknow
的信息了。
说明执⾏在⽅法上声明
@CachePut
会⾃动执⾏⽅法,并将结果存⼊缓存。
@CachePut
配置⽅法
- value 缓存的名称。
- key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照⽅法的所有 参数进⾏组合。
- condition 缓存的条件,可以为空,使⽤ SpEL 编写,返回 true 或者 false,只有为 true 才进⾏缓存。
可以看出
@CachePut
的参数和使⽤⽅法基本和
@Cacheable
⼀致。
@CachePut
也可以标注在类上和⽅法上。
@CacheEvict
@CacheEvict
是⽤来标注在需要清除缓存元素的⽅法或类上的,当标记在⼀个类上时表示其中所有的⽅法的
执⾏都会触发缓存的清除操作。
@CacheEvict
可以指定的属性有
value
、
key
、
condition
、
allEntries
和
beforeInvocation
,其中
value
、
key
和
condition
的语义与
@Cacheable
对应的属性类似。
即
value
表示清除操作是发⽣在哪些
Cache
上的(对应
Cache
的名称);
key
表示需要清除的是哪个
key
,
如未指定则会使⽤默认策略⽣成的
key
;
condition
表示清除操作发⽣的条件。下⾯来介绍⼀下新出现的两个
GitChat
属性
allEntries
和
beforeInvocation
。
allEntries 属性
allEntries
是
boolean
类型,表示是否需要清除缓存中的所有元素,默认为
false
,表示不需要。当指定了
allEntries
为
true
时,
Spring Cache
将忽略指定的
key
,有的时候我们需要
Cache
⼀下清除所有的元素,这
⽐⼀个⼀个清除元素更有效率。
在上⼀个⽅法中我们使⽤注解:
@CachePut(value="usersCache",key="#nickname")
来更新缓
存,但如果不写
key="#nickname"
,
Spring Boot
会以默认的
key
值去更新缓存,导致最上⾯的
getUsers()
⽅法并没有获取最新的数据。但是现在我们使⽤
@CacheEvict
就可以解决这个问题了,它会将所
有以
usersCache
为名的缓存全部清除。我们来看个例⼦:
@RequestMapping("/allEntries")
@CacheEvict(value="usersCache", allEntries=true)
public List<User> allEntries(String nickname) {
List<User> users=userRepository.findByNickname(nickname);
System.out.println("执⾏了数据库操作");
return users;
}
⼿动修改⽤户表的相关信息,⽐如注册时间。在浏览器中输⼊⽹址
http://localhost:8080/getUsers?
nickname=ityouknow
发现缓存中的数据并没有更新,再次访问地址
http://localhost:8080/getUsers?
nickname=ityouknow
会发现数据已经更新,并且输出栏输出
“
执⾏了数据库操作
”
,这表明已经将名为
usersCache
的缓存记录清空了。
beforeInvocation 属性
清除操作默认是在对应⽅法成功执⾏之后触发的,即⽅法如果因为抛出异常⽽未能成功返回时也不会触发清
除操作。使⽤
beforeInvocation
可以改变触发清除操作的时间,当我们指定该属性值为
true
时,
Spring
会在
调⽤该⽅法之前清除缓存中的指定元素。
@RequestMapping("/beforeInvocation")
@CacheEvict(value="usersCache", allEntries=true, beforeInvocation=true)
public void beforeInvocation() {
throw new RuntimeException("test beforeInvocation");
}
我们来做⼀个测试,在⽅法中添加⼀个异常,访问⽹址
http://localhost:8080/beforeInvocation
查看
usersCache
的缓存是否被更新。
按照上⾯的实验步骤,⼿动修改⽤户表的相关信息,访问⽹址
http://localhost:8080/getUsers?
nickname=ityouknow
发现缓存中的数据并没有更新;再访问⽹址
http://localhost:8080/beforeInvocation
会
报错误,先不⽤管这⾥,再次访问地址
http://localhost:8080/getUsers?nickname=ityouknow
会发现数据已
经更新,并且输出栏输出
“
执⾏了数据库操作
”
。这表明虽然在测试的过程中⽅法抛出了异常,但缓存中名为
GitChat
usersCache
的记录都已被清空。
总结⼀下其作⽤和配置⽅法
@Cacheable 作⽤和配置⽅法
主要针对⽅法配置,能够根据⽅法的请求参数对其结果进⾏缓存:
主要参数 | 解释 | 举例例 |
value | 缓存的名称,在 spring 配置⽂文件中定义,必须指定⾄至少⼀一个 | 如 @Cacheable(value="mycache") 或者 @Cacheable(value= {"cache1","cache2"} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不不指定,则缺省按照⽅方法的所 有参数进⾏行行组合 | 如@Cacheable(value="testcache", key="#userName") |
condition | 缓存的条件,可以为空,使⽤用 SpEL 编写,返回true 或者 false,只有为 true 才进⾏行行缓存 | 如@Cacheable(value="testcache", condition="#userName.length()>2") |
@CachePut 作⽤和配置⽅法
@CachePut
的作⽤
是主要针对⽅法配置,能够根据⽅法的请求参数对其结果进⾏缓存,和
@Cacheable
不
同的是,它每次都会触发真实⽅法的调⽤。
主要参数 | 解释 | 举例例 |
value | 缓存的名称,在 spring 配置⽂文件中定义,必须指定⾄至少⼀一个 | 如 @Cacheable(value="mycache") 或者 @Cacheable(value= {"cache1","cache2"} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不不指定,则缺省按照⽅方法的所 有参数进⾏行行组合 | 如@Cacheable(value="testcache", key="#userName") |
condition | 缓存的条件,可以为空,使⽤用 SpEL 编写,返回true 或者 false,只有为 true 才进⾏行行缓存 | 如@Cacheable(value="testcache", condition="#userName.length()>2") |
@CacheEvict 作⽤和配置⽅法
主要针对⽅法配置,能够根据⼀定的条件对缓存进⾏清空。
主要参数 | 解释 | 举例例 |
value | 缓存的名称,在 spring 配置⽂文件中定义, 必须指定⾄至少⼀一个 | 如 @CachEvict(value="mycache") 或者 @CachEvict(value= {"cache1","cache2"} |
key | 缓存的 key,可以为空,如果指定要按照SpEL 表达式编写,如果不不指定,则缺省按照⽅方法的所有参数进⾏行行组合 | 如@CachEvict(value="testcache", key="#userName") |
condition | 缓存的条件,可以为空,使⽤用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 | 如 @CachEvict(value="testcache", condition="#userName.length()>2") |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则⽅方法调⽤用后将⽴立即清空所有缓存 | 如@CachEvict(value="testcache", allEntries=true) |
beforeInvocation | 是否在⽅方法执⾏行行前就清空,缺省为 false, 如果指定为 true,则在⽅方法还没有执⾏行行的时候就清空缓存,缺省情况下,如果⽅方法执⾏行行抛出异常,则不不会清空缓存 | 如@CachEvict(value="testcache", beforeInvocation=true) |
@Cacheable
、
@CacheEvict
、
@CachePut
三个注解⾮常灵活,满⾜了我们对数据缓存的绝⼤多数使
⽤场景,并且使⽤起来⾮常的简单⽽⼜强⼤,在实际⼯作中我们可以灵活搭配使⽤。
总结
Spring
提供了基于注释驱动的
Spring Cache
,它是⼀个对缓存使⽤的抽象,将我们常⽤的缓存策略都进⾏了
⾼度抽象,让我们在项⽬中使⽤时只需要添加⼏个注解,即可完成⼤多数缓存策略的实现。
Spring Boot
Starter Cache
是
Spring Boot
提供给我们在
Spring Boot
中使⽤
Spring Cache
的
Starter
包,集成后⽅便在
Spring Boot
体系中使⽤缓存。