本文复制粘贴整理排版自:
如有侵权, 请联系本人删除.
功能说明
@Cacheable
注解在方法上, 表示该方法的返回结果是可以缓存的. 也就是说, 该方法的返回结果会放在缓存中, 以便于以后使用相同的参数调用该方法时, 会返回缓存中的值, 而不会实际执行该方法.
注意, 这里强调了一点: 参数相同. 这一点应该是很容易理解的, 因为缓存不关心方法的执行逻辑, 它能确定的是: 对于同一个方法, 如果参数相同, 那么返回结果也是相同的. 但是如果参数不同, 缓存只能假设结果是不同的, 所以对于同一个方法, 你的程序运行过程中, 使用了多少种参数组合调用过该方法, 理论上就会生成多少个缓存的 key(当然, 这些组合的参数指的是与生成 key 相关的). 下面来了解一下 @Cacheable
的一些参数:
cacheNames & value
@Cacheable 提供两个参数来指定缓存名:value, cacheNames, 二者选其一即可. 这是 @Cacheable 最简单的用法示例:
@Override
@Cacheable("menu")
public Menu findById(String id) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}
在这个例子中,findById
方法与一个名为 menu
的缓存关联起来了. 调用该方法时, 会检查 menu
缓存, 如果缓存中有结果, 就不会去执行方法了.
关联多个缓存名
其实, 按照官方文档,@Cacheable
支持同一个方法关联多个缓存. 这种情况下, 当执行方法之前, 这些关联的每一个缓存都会被检查, 而且只要至少其中一个缓存命中了, 那么这个缓存中的值就会被返回. 示例:
@Override
@Cacheable({"menu", "menuById"})
public Menu findById(String id) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}
@GetMapping("/findById/{id}")
public Menu findById(@PathVariable("id")String id){
Menu menu0 = menuService.findById("fe278df654adf23cf6687f64d1549c0a");
Menu menu2 = menuService.findById("fb6106721f289ebf0969565fa8361c75");
return menu0;
}
为了直观起见, 直接将 id 参数写到代码里. 现在, 我们来测试一下, 看一下结果:
key & keyGenerator
一个缓存名对应一个被注解的方法, 但是一个方法可能传入不同的参数, 那么结果也就会不同, 这应该如何区分呢? 这就需要用到 key . 在 spring 中, key 的生成有两种方式: 显式指定和使用 keyGenerator
自动生成.
KeyGenerator 自动生成
当我们在声明 @Cacheable 时不指定 key 参数, 则该缓存名下的所有 key 会使用 KeyGenerator 根据参数 自动生成.spring 有一个默认的 SimpleKeyGenerator , 在 spring boot 自动化配置中, 这个会被默认注入. 生成规则如下:
- 如果该缓存方法没有参数, 返回 SimpleKey.EMPTY ;
- 如果该缓存方法有一个参数, 返回该参数的实例 ;
- 如果该缓存方法有多个参数, 返回一个包含所有参数的 SimpleKey ;
默认的 key 生成器要求参数具有有效的 hashCode() 和 equals() 方法实现. 另外, keyGenerator 也支持自定义, 并通过 keyGenerator 来指定. 关于 KeyGenerator 这里不做详细介绍, 有兴趣的话可以去看看源码, 其实就是使用 hashCode 进行加乘运算. 跟 String 和 ArrayList 的 hash 计算类似.
显式指定 key
相较于使用 KeyGenerator 生成, spring 官方更推荐显式指定 key 的方式, 即指定 @Cacheable
的 key 参数.
即便是显式指定, 但是 key 的值还是需要根据参数的不同来生成, 那么如何实现动态拼接呢?SpEL(Spring Expression Language, Spring 表达式语言) 能做到这一点. 下面是一些使用 SpEL 生成 key 的例子.
@Override
@Cacheable(value = {"menuById"}, key = "#id")
public Menu findById(String id) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}
@Override
@Cacheable(value = {"menuById"}, key = "'id-' + #menu.id")
public Menu findById(Menu menu) {
return menu;
}
@Override
@Cacheable(value = {"menuById"}, key = "'hash' + #menu.hashCode()")
public Menu findByHash(Menu menu) {
return menu;
}
显示指定的好处在于, 直观明了, 看到代码就能想象生成的 key 是什么样. 而且 SpEL 也很强大. 关于 SpEL 的详细用法, 这里不详述, 可以参考官方文档:
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions
注意: 官方说 key 和 keyGenerator 参数是互斥的, 同时指定两个会导致异常.
cacheManager & cacheResolver
CacheManager
, 缓存管理器是用来管理 (检索) 一类缓存的. 通常来讲, 缓存管理器是与缓存组件类型相关联的. 我们知道, spring 缓存抽象的目的是为使用不同缓存组件类型提供统一的访问接口, 以向开发者屏蔽各种缓存组件的差异性. 那么 CacheManager
就是承担了这种屏蔽的功能.spring 为其支持的每一种缓存的组件类型提供了一个默认的 manager, 如:RedisCacheManager
, EhCacheManager
.
CacheResolver
, 缓存解析器是用来管理缓存管理器的, CacheResolver 保持一个 cacheManager 的引用, 并通过它来检索缓存.CacheResolver 与 CacheManager 的关系有点类似于 KeyGenerator 跟 key.spring 默认提供了一个 SimpleCacheResolver
, 开发者可以自定义并通过 @Bean
来注入自定义的解析器, 以实现更灵活的检索.
大多数情况下, 我们的系统只会配置一种缓存, 所以我们并不需要显式指定 cacheManager 或者 cacheResolver. 但是 spring 允许我们的系统同时配置多种缓存组件, 这种情况下, 我们需要指定. 指定的方式是使用 @Cacheable
的 cacheManager
或者 cacheResolver
参数.
注意: 按照官方文档, cacheManager 和 cacheResolver 是互斥参数, 同时指定两个可能会导致异常.
sync
是否同步, true/false. 在一个多线程的环境中, 某些操作可能被相同的参数并发地调用, 这样同一个 value 值可能被多次计算 (或多次访问 db), 这样就达不到缓存的目的. 针对这些可能高并发的操作, 我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住, 这样就只能有一个线程计算操作的结果值, 而其它线程需要等待, 这样就避免了 n-1 次数据库访问.
sync = true
可以有效的避免缓存击穿的问题.
condition
调用前判断, 缓存的条件. 有时候, 我们可能并不想对一个方法的所有调用情况进行缓存, 我们可能想要根据调用方法时候的某些参数值, 来确定是否需要将结果进行缓存或者从缓存中取结果. 比如当我根据年龄查询用户的时候, 我只想要缓存年龄大于 35 的查询结果. 那么 condition 能实现这种效果.
condition 接收一个结果为 true 或 false 的表达式, 表达式同样支持 SpEL . 如果表达式结果为 true, 则调用方法时会执行正常的缓存逻辑 (查缓存-有就返回-没有就执行方法-方法返回不空就添加缓存); 否则, 调用方法时就好像该方法没有声明缓存一样 (即无论传入了什么参数或者缓存中有些什么值, 都会执行方法, 并且结果不放入缓存). 下面举个例子:
我们首先定义一个带条件的缓存方法:
@Override
@Cacheable(value = {"menuById"}, key = "#id", condition = "#conditionValue > 1")
public Menu findById(String id, Integer conditionValue) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}
然后分两种情况调用 (为了直观可见, 直接将 id 写在代码中):
@GetMapping("/findById/{id}")
public Menu findById(@PathVariable("id")String id){
Menu menu0 = menuService.findById("fe278df654adf23cf6687f64d1549c0a", 0);
Menu menu2 = menuService.findById("fb6106721f289ebf0969565fa8361c75", 2);
return menu0;
}
然后我们请求一下, 看看缓存中的结果和控制台打印:
可以看到, 两次请求都执行方法 (因为原来缓存中都没有数据), 但是只有"微服务测试 2"缓存了. 这说明, 只有满足 condition 条件的调用, 结果才会被缓存. 接下来我们再请求一遍, 看下结果和打印:
可以看到,"微服务测试 2"由于已经有了缓存, 所以没有再执行方法体. 而"微服务测试 0"又一次执行了.
unless
执行后判断, 不缓存的条件.unless 接收一个结果为 true 或 false 的表达式, 表达式支持 SpEL. 当结果为 true 时, 不缓存. 举个例子:
@Override
@Cacheable(value = {"menuById"}, key = "#id", unless = "#result.type == 'folder'")
public Menu findById(String id) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}
然后调用该方法:
@GetMapping("/findById/{id}")
public Menu findById(@PathVariable("id")String id){
Menu menu0 = menuService.findById("fe278df654adf23cf6687f64d1549c0a");
Menu menu2 = menuService.findById("fb6106721f289ebf0969565fa8361c75");
return menu0;
}
可以看到, 两次都执行了方法体 (其实, unless 条件就是在方法执行完毕后调用, 所以它不会影响方法的执行), 但是结果只有 menu.type = 'page'
的缓存了, 说明 unless 参数生效了.
condition VS unless ?
既然 condition 和 unless 都能决定是否进行缓存, 那么同时指定这两个参数并且结果相冲突的时候, 会怎么样呢? 我们来试一试.
首先清除 redis 数据, 然后在缓存方法上加上 condition="true"
, 如:
@Override
@Cacheable(value = {"menuById"}, key = "#id", condition = "true", unless = "#result.type == 'folder'")
public Menu findById(String id) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}
其它代码不变, 我们来看一下缓存结果和打印, 可以看到, 虽然两次调用都执行了, 但是,type='folder'
的还是被排除了. 说明这种情况下, unless 比 condition 优先级要高. 接下来我们把 condition="false"
, 再来试试, 结果:
可以看到, 两次调用的结果都没有缓存. 说明在这种情况下, condition 比 unless 的优先级高. 总结起来就是:
- condition 不指定相当于 true, unless 不指定相当于 false
- 当 condition = false, 一定不会缓存;
- 当 condition = true, 且 unless = true, 不缓存;
- 当 condition = true, 且 unless = false, 缓存;
缓存清除
概述
@CacheEvict
是用来标注在需要清除缓存元素的方法或类上的.
当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作.
@CacheEvict 可以指定的属性有 value
, key
, condition
, allEntries
和 beforeInvocation
. 其中 value
, key
和 condition
的语义与 @Cacheable
对应的属性类似.
即 value
表示清除操作是发生在哪些 Cache 上的 (对应 Cache 的名称);key
表示需要清除的是哪个 key, 如未指定则会使用默认策略生成的 key;
condition
表示清除操作发生的条件. 下面我们来介绍一下新出现的两个属性 allEntries
和beforeInvocation
.
allEntries 属性
allEntries 是 boolean 类型, 表示是否需要清除缓存中的所有元素. 默认为 false, 表示不需要. 当指定了 allEntries 为 true 时, 清除缓存中的所有元素, Spring Cache 将忽略指定的 key. 有的时候我们需要 Cache 一下清除所有的元素, 这比一个一个清除元素更有效率.
@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
beforeInvocation 属性
清除操作默认是在对应方法成功执行之后触发的, 即方法如果因为抛出异常而未能成功返回时也不会触发清除操作. 使用 beforeInvocation 可以改变触发清除操作的时间, 当我们指定该属性值为 true 时, Spring 会在调用该方法之前清除缓存中的指定元素.
@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
System.out.println("delete user by id: " + id);
}
踩坑记录
Cacheable 是基于代理的, 所以不要在被代理的类中直接调用这个方法, 原理同 @Transactional
一样.
文章评论