缓存场景
1. 重复使用
频繁使用数据,为了避免多次查询数据库,浪费时间、占用资源、造成压力,应使用缓存。
应该对查询数据作特殊限制,防止
缓存击穿
2. 暂缓数据
中间数据,存在一段时间,但是不必持久化,使用缓存更方便。
在生产环境中,产生的中间数据在一段时间内可复用,避免重复计算造成不必要的开销。
即使是上述的基础数据,即使使用缓存,为了避免
更新
无法生效的情况,也应该设置过期
时间。从这方面考虑,两者的体现一致,只是
数据来源
存在差异而已。
JSR107
接口 | 作用 |
---|---|
CachingProvider | 管理CacheManager |
CacheManager | 管理Cache |
Cache | 管理Entry |
Entry | key -value 存储缓存数据 |
Expiry | 设置Entry 过期时间 |
- 管理含义包括
创建
,配置
,获取
等,完全控制下一级- 上对下为一对多,下对上为一对一
Spring-Boot缓存接口
组件 | 作用 |
---|---|
Cache | 缓存接口 |
CacheManager | 管理Cache |
Cacheable | 标记方法,对返回结果进行缓存 |
CacheEvict | 清空缓存 |
CachePut | 保证方法调用,且希望结果被缓存 |
EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存key 生成策略 |
serialize | 缓存value 序列化策略 |
@EnableCaching
@MapperScan("com.godme.cache.mapper")
@SpringBootApplication
@EnableCaching
public class GodmeApplication {
public static void main(String[] args) {
SpringApplication.run(GodmeApplication.class, args);
}
}
@EnableCaching
:必须先声明@EnableCaching
开启,才能使用缓存注解
@Cacheable
@Component
public class StudentDao {
StudentMapper studentMapper;
@Cacheable
public Student queryStudent(Integer id){
System.err.println("查询");
return studentMapper.getStudent(id);
}
}
标记方法,会把运行结果缓存起来。
cacheNames
/value
指定缓存名字
@Cacheable(cacheNames = {"a", "b"} )
支持复数个名称
key
缓存都是
key-value
,这就是其中的key
不指定的情况下默认使用的是方法参数
id
的值
(不是参数名id
,而是id的具体取值
)指定时可以是具体字符串,也可以是
spEL
表达式
@Cacheable(key = "#{methodName} + '[' + #{id} + ']'")
属性 示例 描述 methodName
#root.methodName
方法名称 method
#root.method.name
方法对象 target
#root.target
目标对象 targetClass
#root.targetClass
目标类 args
#root.args[0]
参数列表,数组 caches
#root.caches[0].name
缓存列表, cacheNames
数组args-name
#a0
,#p0
参数名称,下标指定 args
,param
result
#result
方法返回值
keyGenerator
key
到的生成器key
和keyGenerator
冲突,二选一使用,优先使用key
// 自定义KeyGenerator @Configuration public class MyConfig { @Bean("godmeKeygenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { return method.getName() + "["+ Arrays.asList(params).toString()+"]"; } }; } }
// 指定keyGenerator @Cacheable(keyGenerator = "godmeKeyGenerator")
cacheManager
指定
cacheManger
,把值存入或从指定cacheManager
获取
cacheResolver
缓存解析器
,同cacheManager
,管理缓存集合cacheManager
和cacheResolver
二选一缓存集合
具体看底层实现,默认使用currentMap
condition
- 指定条件才进行缓存
- 支持
spEL
表达式
@Cacheable(condition = "#id>0")
unless
- 同condition,操作相反,满足条件则不缓存
@Cacheable(unless = "#id<0")
- 可获取结果判断
@Cacheable(unless = "#result == null")
sync
是否采用异步模式,
true
,false
@Cacheable(sync = true)
- 感情纠葛
key
和KeyGenerator
二取其一condition
和unless
含义相反
- 默认缓存
contdition
第一层过滤,筛选需要缓存的unless
二次过滤,没有condition
相当于第一层,满足条件的不缓存,即使满足condition
sync
和unless
不共存,使用了sync
(true
),不能设置unless
(不生效)
@CachePut
@CachePut(cacheNames = "student", key = "#{student.id}")
public int updateStudent(Student student){
System.err.println("更新");
return studentMapper.updateStudent(student);
}
- 执行顺序
@Cacheable
之前说了
@Cacheable
但是没有执行顺序,但是回顾一下它的作用,就很轻松的明白过来。缓存执行结果,下次调用不用再执行,而是直接获取缓存。
所以,每次调用之前它都会优先查缓存,没有查到的情况下,执行方法后再缓存。
因此,执行步骤为
- 查询缓存
- 方法执行(未查询到缓存)
- 结果缓存(为查询到缓存)
- 结果返回
有缓存,那就是直接的两步,只有缓存不存在时分四步走
CachePut
更新缓存,就是它的作用了。
所以是先执行方法,然后再把结果
更新
到缓存中。所谓
更新
也就是没有就添加
,有了就替换
因此,执行步骤为
- 方法执行
- 缓存更新
- 结果返回
所以,不论是否存在缓存,始终都是三步走,关键区别在于缓存是
新增
,还是更新
- 更新缓存
缓存的确会
写入
,具体是更新
或者新增
不用在意。关键在于缓存的
获取
。
@Cacheable
是能获取缓存的,我们更新缓存,主要也是要让缓存能够查
的准确。所以,要严格区分
key
@CachePut(cacheNames = "student", key = "#{student.id}")
还记得的话,
key
默认的是参数值,如果key
不统一,缓存就会是两个毫不相干的路人。要确保即使方法不同,参数各异,采取的
key
也要一致。保证
更新
写对了地方,保证查询
查对了地方。
- 其他属性
特殊的就只是
#result
而已,在条件判断时需要对result
进行判断。如果方法不执行,
result
是不存在的。存运行流程上分析,
CachePut
是完全没问题的。但是
Cacheable
在方法第一次执行时,result
是不存在的,无缓存情况下,是不能用的。这点区别要牢记,在
Cacheable
上尽量别用result
@CacheEvict
@CacheEvict(key = "#{id}")
public int deleteStudent(Integer id){
System.err.println("删除");
return studentMapper.deleteStudent(id);
}
key
老生常谈,注意
key
的一致性,别该删的没删,不该删的全没了,那就该跑路了。
allEntries
布尔值,默认为
false
。当
false
的时候,清除缓存是按照key
来进行清除的,清除Cache
下的单一Entry
。当
true
的时候,通过cacheNames
或value
来进行清除,会清除指定名字Cache
下的全部Entry
。
beforeInvocation
布尔值,默认
false
这个涉及的就是清除顺序了。
清除的时候只和
key
或者cacheNames
相关,对于方法的执行步骤上好像没有依赖性。默认情况下,在方法执行完毕之后再进行缓存的清除,这就有了一个逻辑的连贯性。
当方法异常或其他情况导致方法中断时,缓存就可能得不到清除。
设置为
true
,就会在方法之前进行缓存清除,保证缓存的事务完整。
@Caching
@Cacheable(key = "#{id}")
@CachePut(key = "#{id}")
public Student queryStudent(Integer id){
System.err.println("查询");
return studentMapper.getStudent(id);
}
恩~~~,感觉没啥用?
那考虑一下业务场景:我们通过
id
查询出了一个对象,但是查询办法有好多。说不定,有通过
name
查询,通过parentName
?motherName
?fatherName
?
@Cacheable(key = "#{id}") @CachePut(key = "#{parentName,motherName,fatherName}") public Student queryStudent(Integer id){ System.err.println("查询"); return studentMapper.getStudent(id); }
这样一来,只要查询一遍对象,其他方法都可以从缓存里面取出来了,这不是好牛逼了么。
这的确很好用,但是有两个问题:
- 如果有不同配置,但是相同注解只能标注一个
- 好丑,没有统一管理
@Cacheable(key = "#{id}") @CachePut(value="parent", key = "#{parentName}") @CachePut(value="mother", key = "#{motherName}") @CachePut(value="father", key = "#{fatherName}") public Student queryStudent(Integer id){ System.err.println("查询"); return studentMapper.getStudent(id); }
@CachePut
都是红的,怎么搞?
@Caching( cacheable = { @Cacheable(value = "a", key = "#{id}"), @Cacheable(value = "b", key = "#{name}") }, put = { @CachePut(value = "a", key = "#{id}"), @CachePut(value = "b", key = "#{name}") }, evict = {} ) public Student queryStudent(Integer id){ System.err.println("查询"); return studentMapper.getStudent(id); }
这样一来,就可以同时配置多个注解了,还能统一管理,美滋滋。
不过我发现一个坑,有缓存的情况下
put
生效了怎么办,evict
呢?
condition
引起了我的注意,条件判断什么的这里面都能够完成啊。不过具体办法加上结合业务逻辑,估计就得靠自己想办法了。
@CacheConfig
@CacheConfig(cacheNames = "student", keyGenerator = "godmeKeyGenerator")
@Component
public class StudentDao {
...
}
正和上面所提到的一样,写太多太繁杂了,我们要的只是葫芦。
通过
Cacheconfig
对Class
进行标记,把公共的东西都抽取进来,里面的注解就只用关心条件逻辑了。
public @interface CacheConfig { String[] cacheNames() default {}; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; }
可抽取的公共配置都在这了,然后你就不必思考–我打的缓存究竟是在
- 哪个
CacheManager
- 那个
Cache
- …
之类的问题了
恩~~~
- 最后两个只是管理用的,无涉缓存原理
- 缓存步骤大同小异,缓存实现千差万别