Spring Cache 使用教程之二
大纲
前言
官方文档
使用 @Cacheable 注解
@Cacheable
可以将方法运行的结果进行缓存,在缓存时效内再次调用该方法时不会调用方法本身,而是直接从缓存获取结果并返回给调用方。
属性介绍
属性名 | 描述 |
---|---|
value / cacheNames | 指定缓存的名称,Spring Cache 使用 CacheManage 管理多个缓存组件 Cache,这里的 Cache 组件就是根据该名称进行区分的,它负责对缓存执行真正的 CRUD 操作 |
key | 缓存数据时 Key 的值,默认是使用方法参数的值,可以使用 SpEL 表达式计算 Key 的值 |
keyGenerator | 缓存 Key 的生成策略,它和 key 属性互斥使用(只能二选一) |
cacheManager | 指定缓存管理器(如 ConcurrentHashMap、Redis 等) |
cacheResolver | 作用和 cacheManager 属性一样,两者只能二选一 |
condition | 指定缓存的条件(满足什么条件才缓存),可用 SpELl 表达式(如 #id>0 ,表示当入参 id 大于 0 时才缓存) |
unless | 否定缓存,即满足 unless 指定的条件时,方法的结果不进行缓存,使用 unless 时可以在调用的方法获取到结果之后再进行判断(如 #result == null ,表示如果结果为 null 时不缓存) |
sync | 是否使用异步模式进行缓存,默认值是 false |
在一个多线程的环境中,某些操作可能会被相同的参数并发地调用,同一个 value
值可能被多次计算(或多次访问数据库),这样就达不到缓存的目的。针对这些可能高并发的操作,可以使用 sync
属性来告诉底层的缓存提供者将缓存的入口锁住,这样在同一时刻就只能有一个线程计算操作的结果值,而其它线程则需要等待。当 sync
的值为 true
时,相当于同步操作,可以有效地避免出现缓存击穿的问题,关于缓存击穿的介绍可以点击 这里。
特别注意
- 1、即满足
condition
又满足unless
条件的情况下,不会缓存数据 - 2、使用异步模式(
sync=true
)进行缓存时,unless
条件将不会生效 - 3、
condition
不指定相当于true
,而unless
不指定相当于false
- 4、
condition
属性使用的 SpEL 表达式只有#root
和获取方法参数类的 SpEL 表达式,不能使用带返回结果的表达式(如#result
),因此condition = "#result != null"
会导致所有对象都不写入缓存,每次都要查询数据库。
使用案例
1 |
|
指定 Key
@Cacheable
注解有一个属性 key
可以用于直接定义缓存 Key,该属性不是必填项。如果为空,则会使用默认的 Key 生成器进行生成。默认的 Key 生成器要求方法参数具有有效的 hashCode()
和 equals()
方法实现。值得一提的是,key
属性的值支持使用 SpEL 表达式。使用方法参数作为 Key 时,可以直接使用 #参数名
或者 #p参数索引
的 SpEL 表达式来引用,以下的写法都是合法的。
1 |
|
使用 @CachePut 注解
与 @Cacheable
注解不同的是使用 @CachePut
注解标注的方法,在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式写入指定的缓存中。@CachePut
注解一般用于更新缓存数据,相当于缓存使用的是写模式中的双写模式。
属性介绍
@CachePut
注解所具有的属性与 @Cacheable
注解相同,这里不再累述。
使用案例
1 |
|
使用 @CacheEvict 注解
标注了 @CacheEvict
注解的方法在被调用时,会从缓存中移除已存储的数据。@CacheEvict
注解一般用于删除缓存数据,相当于缓存使用的是写模式中的失效模式。
属性介绍
属性名 | 描述 |
---|---|
value / cacheNames | 缓存的名称 |
key | 缓存的键 |
allEntries | 是否根据缓存名称清空所有缓存数据,默认值为 false ,当值指定为 true 时,Spring Cache 将忽略注解上指定的 key 属性 |
beforeInvocation | 是否在方法执行之前就清空缓存,默认值为 false |
清除缓存的操作默认是在对应方法成功执行之后才触发的,即方法的执行如果因为抛出异常而未能成功返回时也不会触发清除操作。使用 beforeInvocation
属性可以改变触发清除操作执行的时机,当指定该属性的值为 true
时,Spring 会在调用该方法之前清除缓存中的数据。值得一提的是,在方法调用之前还是之后清除缓存的区别在于方法调用时是否会出现异常,若不出现异常,这两者之间没有区别,若出现异常,设置为在方法调用之后清除缓存将不起作用,因为方法调用失败了。
使用案例
1 |
|
使用 @Caching 注解
@Caching
注解用于在一个方法或者类上,同时指定多个 Spring Cache 相关的注解。
属性介绍
属性名 | 描述 |
---|---|
cacheable | 用于指定 @Cacheable 注解 |
put | 用于指定 @CachePut 注解 |
evict | 用于指定 @@CacheEvict 注解 |
使用案例
1 |
|
使用 @CacheConfig 注解
@CacheConfig
注解标注在类上,用于抽取 Spring Cache 相关注解的公共配置,可抽取的公共配置包括缓存名称、主键生成器、缓存管理器。比如,在每个 Spring Cache 缓存注解中,往往都指定了缓存名称(value = "stu"
或者 cacheNames = "stu"
)。此时 @CacheConfig
注解可以将它抽离出来,并在整个类上添加 @CacheConfig(value = "stu")
注解之后,每个方法默认都会使用指定的缓存名称 stu
。
属性介绍
属性名 | 描述 |
---|---|
value / cacheNames | 指定缓存的名称,Spring Cache 使用 CacheManage 管理多个缓存组件 Cache,这里的 Cache 组件就是根据该名称进行区分的,它负责对缓存执行真正的 CRUD 操作 |
cacheManager | 缓存管理器 |
keyGenerator | 主键生成器 |
使用案例
1 |
|
使用 SpEL 表达式
SpEL 表达式的语法
Spring Cache 也提供了
root
对象,可以在 SpEL 表达式中直接使用
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | 根对象 | 要调用的方法的名称 | #root.methodName |
method | 根对象 | 正在调用的方法 | #root.method.name |
target | 根对象 | 正在调用的目标对象 | #root.target |
targetClass | 根对象 | 要调用的目标的类 | #root.targetClass |
args | 根对象 | 用于调用目标的参数(作为数组) | #root.args[0] |
caches | 根对象 | 正在调用的方法使用的缓存列表(如 @Cacheable(value={"cache1", "cache2"})) ,则有两个缓存) | #root.caches[0].name |
argument name | 评估背景 | 方法参数名,可以直接使用 #参数名 ,也可以使用 #p0 或 #a0 的形式,0 代表参数的索引 | #iban、#a0、#p0 |
result | 评估背景 | 方法执行后的返回值,仅当方法执行之后的判断有效,如 unless 、cache put 、cache evict (当 beforeInvocation = false) 的表达式 | #result |
SpEL 表达式的使用案例
1 |
|
当需要使用 root
对象的属性作为 Key 时,还可以将 #root
省略掉,因为 Spring Cache 默认使用的就是 root
对象的属性
1 |
|
当需要调用目前类里的方法动态生成 Key 时,在 SpEL 表达式内拼接字符串时,必须使用单引号将字符串包裹起来
1 |
|
自定义缓存配置
防止缓存穿透
为了避免出现缓存穿透,建议让 Spring Cache 将空值也写入缓存,关于缓存穿透的介绍可以点击 这里。
1 | spring: |
指定有效时间
指定缓存数据的有效时间,这样可以让缓存数据过期被删除后,触发主动更新(基于缓存的读模式)。
1 | spring: |
提示
Spring Cache 的注解不支持给缓存单独设置不同的有效时间,若希望像 Redis 一样设置缓存的有效时间,可以参考这篇 博客。
指定 Key 前缀
在不指定 Key 前缀时,Spring Cache 默认会使用缓存的名称作为 Key 前缀。
1 | spring: |
指定 Key 生成器
缓存的本质就是键值对存储模式,每一次方法的调用都需要生成相应的 Key,这样才能操作缓存。若没有给 Spring Cache 的注解(如 @Cacheable
)设置属性 key
,缓存抽象默认会使用 SimpleKeyGenerator
来自动生成 Key,具体源码如下:
- 如果没有方法参数,则直接返回
SimpleKey.EMPTY
- 如果只有一个方法参数,则直接返回该方法参数
- 若有多个方法参数,则返回包含多个方法参数的 SimpleKey 对象
Spring Cache 也考虑到需要自定义 Key 的生成方式,只需要实现 org.springframework.cache.interceptor.KeyGenerator
接口,然后通过 @Cacheable
注解的 keyGenerator
属性指定 Key 生成器即可。值得一提的是,默认的 Key 生成器要求方法参数具有有效的 hashCode()
和 equals()
方法实现。
1 | /** |
1 | /** |
指定序列化机制
默认情况下,Spring Cache 会使用 JDK 的序列化机制将缓存数据写入 Redis,这样存储在 Redis 里面的就是二进制数据。若希望 Spring Cache 将数据序列化成 JSON 数据再写入 Redis,可以使用以下的配置类。
1 | import org.springframework.boot.autoconfigure.cache.CacheProperties; |
Spring Cache 加载 Redis 缓存配置的流程
- CacheAutoConfiguration --> RedisCacheConfiguration --> 自动配置了 RedisCacheManager --> 初始化所有缓存 --> 每个缓存决定使用什么配置内容 --> 如果 RedisCacheConfiguration 有就用已经有的,没有就用默认的 Redis 配置
- 所以如果想自定义 Redis 缓存配置,只需要在 Spring 容器中放一个 RedisCacheConfiguration,它就会应用到当前 RedisCacheManager 管理的所有缓存分区中
常见问题总结
缓存不生效
在有些情形下,Spring Cache 注解式缓存是不起作用的。比如在同一个 Bean 里的内部方法调用,又或者是子类调用父类中有缓存注解的方法等。后者不起作用是因为缓存切面必须走代理才有效,这时候可以手动使用 CacheManager
来获得缓存效果。
Spring Cache 的不足
读模式
- 读模式下,可能会出现缓存失效的问题,Spring Cache 的解决方案如下
缓存穿透
:查询一个不存在的数据(Null),解决方案是缓存空数据,配置内容是cache-null-values: true
缓存雪崩
:大量缓存同时过期,解决方案是给缓存设置过期时间(或者是随机的过期时间),配置内容是time-to-live: 3600000
缓存击穿
:大量并发请求进来同时查询一个正好过期的数据,解决方案是使用@Cacheable(sync = true)
来实现同步模式的缓存写入,底层是基于 JDK 的synchronized
- 读模式下,可能会出现缓存失效的问题,Spring Cache 的解决方案如下
写模式
- 双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache 暂时没办法解决,其他的解决方案如下
- 加分布式锁(读写锁),只适用于读多写少的业务场景
- 直接查询数据库,不再从缓存获取数据,只适用于读多写多的业务场景
- 使用 Canal 中间件,实时将数据库的数据更新到缓存,会增加系统的复杂性
- 双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache 暂时没办法解决,其他的解决方案如下
总结
- 常规数据(读多写少、即时性与一致性要求不高的数据)完全可以使用 Spring Cache,至于写模式下缓存数据一致性问题的解决,只要缓存数据有设置过期时间就足够了
- 特殊数据(读多写多、即时性与一致性要求非常高的数据),不能使用 Spring Cache,建议考虑特殊的设计(例如使用 Cancal 中间件等)
提示
更多关于缓存写模式、缓存失效、缓存数据一致性、分布式锁的介绍,请点击 这里。