Spring 中基于 Redis 的缓存
Spring Data Redis 提供了 Spring Framework 缓存抽象的实现,位于 org.springframework.data.redis.cache
包中。
一、RedisCacheManager 的使用
要使用 Redis 作为缓存实现,需要在配置中添加 RedisCacheManager
,如下所示:
1 |
|
RedisCacheManager
的行为可以通过 RedisCacheManager.RedisCacheManagerBuilder
进行配置,配置中允许设置默认的 RedisCacheManager
、事务行为和预定义的缓存。
1 | RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory) |
如上例所示,RedisCacheManager
允许在每个缓存基础上进行自定义配置。
RedisCache
的行为由 RedisCacheManager
创建时定义的 RedisCacheConfiguration
决定。该配置允许你设置键的过期时间、前缀以及用于转换二进制存储格式的 RedisSerializer
实现,如下例所示:
1 | RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() |
RedisCacheManager
默认使用一个无锁的 RedisCacheWriter
来读取和写入二进制值。无锁缓存可以提高吞吐量。然而,缺少对条目的锁定可能会导致 Cache
的 putIfAbsent
和 clean
操作出现重叠且非原子的命令,因为这些操作需要发送多个命令到 Redis。相比之下,带锁的实现通过设置一个显式的锁键并检查该键的存在来防止命令重叠,这会导致额外的请求和潜在的命令等待时间。
锁定应用于缓存级别(cache level),而不是每个缓存条目(cache entry)。
可以使用如下方式添加锁定行为:
1 | RedisCacheManager cacheMangager = RedisCacheManager |
默认情况下,缓存条目的任何 key 都会以前缀形式加上实际的缓存名称,后跟两个冒号 ::
。这种行为可以更改为静态前缀或计算前缀。示例如下:
1 | // static key prefix |
缓存实现默认使用 KEYS
和 DEL
来清除缓存。KEYS
在处理大型键空间时可能会导致性能问题。因此,可以使用 BatchStrategy
创建默认的 RedisCacheWriter
,以切换到基于 SCAN
的批处理策略。SCAN
策略要求批处理大小,以避免过多的Redis命令往返:
1 | RedisCacheManager cacheManager = RedisCacheManager |
KEYS
批处理策略支持使用任何驱动程序和 Redis 操作模式(单机、集群)都是完全支持的。SCAN
在使用 Lettuce
驱动程序时是完全支持的。Jedis
仅在非集群模式下支持 SCAN
。
如下表格列出了 RedisCacheManager
的默认设置:
Setting | Value |
---|---|
Cache Writer | Non-locking, KEYS batch strategy |
Cache Configuration | RedisCacheConfiguration#defaultConfiguration |
Initial Caches | None |
Transaction Aware | No |
如下表格列出了 RedisCacheConfiguration
的默认设置:
Key Expiration | None |
---|---|
Cache null |
Yes |
Prefix Keys | Yes |
Default Prefix | The actual cache name |
Key Serializer | StringRedisSerializer |
Value Serializer | JdkSerializationRedisSerializer |
Conversion Service | DefaultFormattingConversionService with default cache key converters |
默认情况下,RedisCache
的统计功能是禁用的。使用 RedisCacheManagerBuilder.enableStatistics()
可以通过 RedisCache#getStatistics()
收集本地命中和未命中的统计数据,返回收集到的数据的快照。
二、Redis 缓存过期时间
不同数据存储之间,时间到闲置(time-to-idle,TTI)和时间到存活(time-to-live,TTL)的定义和行为可能有所不同。
- 时间到存活(TTL)过期:TTL 仅在创建或更新数据访问操作时设置和重置。只要在 TTL 过期超时之前写入条目,包括在创建时,条目的超时时间将重置为配置的 TTL 过期超时时间。例如,如果 TTL 过期超时设置为 5 分钟,那么在条目创建时超时时间将设置为 5 分钟,并且在 5 分钟间隔到期前的任何时候更新条目时,超时时间将重置为 5 分钟。如果在 5 分钟内没有更新,即使条目被读取多次或在 5 分钟间隔内只读取一次,条目仍然会过期。必须写入条目以防止在声明 TTL 过期策略时条目过期。
- 时间到闲置(TTI)过期:TTI 在条目被读取或更新时都会重置,实际上是 TTL 过期策略的扩展。
某些数据存储在配置了 TTL 后,无论对条目进行何种类型的数据访问操作(读取、写入或其他操作),都会使条目过期。在设置的 TTL 过期超时时间之后,条目将从数据存储中被逐出。逐出操作(例如:销毁、失效、溢出到磁盘(对于持久化存储等))是特定于数据存储的。
1、TTL 过期策略
Spring Data Redis 的缓存实现支持缓存条目的时间到存活(TTL)过期。用户可以通过以下两种方式配置 TTL 过期超时时间:
- 使用固定的
Duration
配置 TTL 过期超时时间。 - 通过提供
RedisCacheWriter.TtlFunction
接口的实现,为每个缓存条目动态计算 TTL 过期超时时间。
如果所有缓存条目都应在设定的持续时间后过期,则只需配置一个具有固定 Duration
的 TTL 过期超时,如下所示:
1 | RedisCacheConfiguration fiveMinuteTtlExpirationDefaults = RedisCacheConfiguration.defaultCacheConfig().enableTtl(Duration.ofMinutes(5)); |
但是,如果 TTL 过期超时因缓存条目而异,则必须提供 RediscoacheWriter.TtlFunction
接口的自定义实现:
1 | enum MyCustomTtlFunction implements TtlFunction { |
在内部,固定 Duration
的 TTL 过期时间会被包装在一个 TtlFunction
实现中,该实现返回提供的 Duration
。
然后,就可以使用以下命令在全局基础上配置固定的 Duration
或动态的每个员条目的 Duration
TTL 过期时间:
1 | // Global fixed Duration TTL expiration timeout |
1 | // Global, dynamically computed per-cache entry Duration TTL expiration timeout |
1 | // Global fixed Duration TTL expiration timeout |
2、TTI 过期策略
Redis 本身并不支持真正的、时间到闲置(TTI)过期的概念。然而,使用 Spring Data Redis 的缓存实现,可以实现类似 TTI 过期的行为。
在 Spring Data Redis 的缓存实现中配置 TTI 必须显式启用,即需要选择加入。此外,你还必须提供 TTL 配置,可以使用固定的 Duration
或者上面在 Redis 缓存过期中描述的 TtlFunction
接口的自定义实现。
1 |
|
由于 Redis 服务器没有实现真正的 TTI(时间到闲置)概念,因此只能通过接受过期选项的 Redis 命令来实现 TTI。在 Redis 中,“过期”实际上是一个 TTL(时间到存活)策略。然而,当读取键的值时,可以传递 TTL 过期时间,从而有效地重置 TTL 过期超时时间,这在 Spring Data Redis 的 Cache.get(key)
操作中现在就是这样实现的。
RedisCache.get(key)
是通过调用 Redis 的 GETEX
命令实现的。
Redis GETEX
命令仅在 Redis 6.2.0 及更高版本中可用。因此,如果你使用的是低于 6.2.0 版本的 Redis,则无法使用 Spring Data Redis 的 TTI 过期功能。如果在不兼容的 Redis(服务器)版本上启用 TTI,将会抛出命令执行异常。系统不会尝试确定 Redis 服务器版本是否正确并支持GETEX
命令。
为了在 Spring Data Redis 应用程序中实现类似 TTI(时间到闲置)过期的行为,条目必须在每次读取或写入操作时一致地使用(TTL)过期时间。这一规则没有例外。如果你在 Spring Data Redis 应用程序中混合使用了不同的数据访问模式(例如:缓存、使用
RedisTemplate
调用操作,尤其是使用 Spring Data Repository 的 CRUD 操作),那么访问条目可能不会阻止条目过期,前提是设置了 TTL 过期时间。例如,一个条目可能在@Cacheable
服务方法调用期间(写入缓存时)设置了 TTL 过期时间(即SET <expiration options>
),然后在过期超时前通过 Spring Data Redis Repository 读取(使用GET
且未指定过期选项)。简单的GET
操作(不指定过期选项)不会重置条目的 TTL 过期超时时间。因此,即使条目刚刚被读取,也可能在下一次数据访问操作之前过期。由于这一点无法在 Redis 服务器中强制执行,因此当配置了时间到闲置过期时间时,你的应用程序有责任在适当的地方一致地访问条目,无论是在缓存中还是在缓存之外。
三、Spring 缓存相关代码
1、@EnableCaching 注解
@EnableCaching
用于启用Spring的缓存支持。当你在一个配置类上使用这个注解时,Spring 会启动缓存方面的基础设施,使得你在其他地方使用的缓存注解(如@Cacheable
、@CachePut
、@CacheEvict
等)能够生效。
该注解需要与 @Configuration
注解一起使用。并且必须要注册一个 CacheManager
类型的 Bean,框架无法使用合理的默认值作为约定。@EnableCaching
注解按照类型搜索缓存管理器 Bean,所以缓存管理器 Bean 方法的命名并不重要。
如果想要 @EnableCaching
注解和确切要使用的缓存管理器 Bean 之间建立更直接关系,可以实现 CachingConfigurer
回调接口。这种方式在同一个容器中存在两个 CacheManager
时很有用。
1 |
|
该注解提供了 3 个属性。其中 mode
属性控制通知应该如何应用。默认值为 AdviceMode.PROXY
,此时其他两个属性用于控制代理行为。
请注意,代理模式仅允许拦截通过代理的调用。同一类内的本地调用无法以这种方式拦截;在这种情况下,本地调用上的缓存注解将被忽略,因为 Spring 的拦截器在这种运行时场景下甚至不会启动。对于更高级的拦截模式,可以考虑将此设置为 AdviceMode.ASPECTJ
。
如果 mode
属性被设置为 AdviceMode.ASPECTJ
,则 proxyTargetClass
属性的值将被忽略。另外,在这种情况下,spring-aspects
模块的 JAR 必须存在于类路径上,编译时织入或加载时织入将切面应用于受影响的类。在这种场景中不会涉及代理;本地调用也将被拦截。
mode
属性的取值只有 PROXY
和 ASPECTJ
两个。
2、@Cacheable 注解
该注解表示方法调用的结果(或类中的所有方法)可以被缓存的注解。
每次调用带有此注解的方法时,都会应用缓存行为,检查该方法是否已经使用给定的参数调用过。
默认情况下,会简单地使用方法参数来计算键,但可以通过 key
属性提供 SpEL 表达式,或者使用自定义的 org.springframework.cache.interceptor.KeyGenerator
实现来替换默认的键生成器。
如果在缓存中未找到计算出的键对应的值,目标方法将被调用,并且返回的值将被存储在关联的缓存中。请注意,java.util.Optional
返回类型会自动解包。如果存在 Optional
值,它将被存储在关联的缓存中;如果 Optional
值不存在,则 null
将被存储在关联的缓存中。
此注解可以用作元注解,以创建具有属性覆盖的自定义复合注解。
1 |
|
2.1 cacheNames 属性
方法调用结果存储的缓存名称。
名称可用于确定目标缓存,通过配置的 cacheResolver()
解析,通常委托给org.springframework.cache.CacheManager.getCache
。
这通常是一个单一的缓存名称。如果指定了多个名称,它们将按定义的顺序检查缓存命中,并且所有缓存都将接收相同的新缓存值的存储/删除请求。
请注意,异步/反应式缓存访问可能不会完全咨询所有指定的缓存,具体取决于目标缓存。在缓存未命中较晚确定的情况下(例如使用 Redis),不会再咨询进一步的缓存。因此,在异步缓存模式设置中指定多个缓存名称只有在缓存未命中较早确定的情况下才有意义(例如使用 Caffeine)。
2.2 key 属性
用于动态计算键的 Spring 表达式语言(SpEL)表达式。
默认值为 ""
,这意味着所有方法参数都被视为键,除非已配置了自定义的 keyGenerator
。
SpEL 表达式会在一个专门的上下文中进行评估,该上下文提供了以下元数据:
#root.method
、#root.target
和#root.caches
分别引用方法、目标对象和受影响的缓存。- 也有方法名的快捷方式
#root.methodName
和目标类的快捷方式#root.targetClass
。 - 方法参数可以通过索引访问。例如,第二个参数可以通过
#root.args[1]
、#p1
或#a1
访问。如果参数名信息可用,也可以通过名称访问参数。
2.3 keyGenerator 属性
要使用的自定义 org.springframework.cache.interceptor.KeyGenerator
的 Bean 名称。
与 key
属性互斥。
2.4 cacheManager 属性
要用于创建默认 org.springframework.cache.interceptor.CacheResolver
的自定义 org.springframework.cache.CacheManager
的 Bean 名称,前提是尚未设置 CacheResolver
。
与 cacheResolver
属性互斥。
2.5 condition 属性
用于使方法缓存条件化的 Spring 表达式语言(SpEL)表达式。
如果条件评估为 true
,则缓存方法返回结果。 默认值为 ""
,这意味着方法结果总是被缓存。
SpEL 表达式会在一个专门的上下文中进行评估,该上下文提供了以下元数据:
#root.method
、#root.target
和#root.caches
分别引用方法、目标对象和受影响的缓存。- 也有方法名的快捷方式
#root.methodName
和目标类的快捷方式#root.targetClass
。 - 方法参数可以通过索引访问。例如,第二个参数可以通过
#root.args[1]
、#p1
或#a1
访问。如果参数名信息可用,也可以通过名称访问参数。
2.6 unless 属性
用于否决方法缓存的 Spring 表达式语言(SpEL)表达式。如果条件评估为 true
,则否决缓存结果。
与 condition
不同,此表达式是在方法被调用后评估的,因此可以引用结果。
默认值为 ""
,意味着永远不会否决缓存。
SpEL 表达式会在一个专门的上下文中进行评估,该上下文提供了以下元数据:
#result
用于引用方法调用的结果。对于支持的包装器(如Optional
),#result
引用的是实际的对象,而不是包装器。#root.method
、#root.target
和#root.caches
分别引用方法、目标对象和受影响的缓存。- 也有方法名的快捷方式
#root.methodName
和目标类的快捷方式#root.targetClass
。 - 方法参数可以通过索引访问。例如,第二个参数可以通过
#root.args[1]
、#p1
或#a1
访问。如果参数名信息可用,也可以通过名称访问参数。
2.7 sync 属性
如果多个线程尝试为同一个键加载值,则同步底层方法的调用。同步带来了一些限制:
- 不支持
unless
属性 - 只能指定一个缓存
- 不能结合其他任何缓存相关操作
这实际上只是一个提示,所选的缓存提供者可能实际上并不支持同步方式。请查阅你的提供者文档以了解实际的语义。
2.8 cacheResolver 属性
要使用的自定义 org.springframework.cache.interceptor.CacheResolver
的Bean名称。
3、@CachePut 注解
该注解表示一个方法(或类上的所有方法)触发缓存存储操作的注解。
与 @Cacheable
注解不同,此注解不会导致被注解的方法被跳过。相反,它总是会导致方法被调用,并且如果 condition()
和 unless()
表达式匹配了条件,则将方法的结果存储在关联的缓存中。即无论缓存中是否存在方法参数对应的值,都会正在执行方法并将结果存储在缓存中。
请注意,Java 8 的 Optional
返回类型会被自动处理,如果存在内容,则将其存储在缓存中。 此注解可以用作元注解,以创建具有属性覆盖的自定义组合注解。
1 |
|
4、@CacheEvict 注解
表示一个方法(或类上的所有方法)触发缓存清除操作的注解。 此注解可以用作元注解,以创建具有属性覆盖的自定义组合注解。
该注解用于清除缓存中的条目。可以从缓存中移除一个或多个条目。可以配置 allEntries
属性来清除整个缓存,或配置 beforeInvocation
属性来在方法调用之前清除缓存。
1 |
|
4.1 allEntries 属性
是否清除缓存中的所有条目。
默认情况下,只移除与关联键对应的值。
注意,将此参数设置为 true
并指定键是不允许的。
4.2 beforeInvocation 属性
是否应在方法调用之前进行清除操作。
将此属性设置为 true
,会导致无论方法结果如何(即,无论方法是否抛出异常)都会进行清除操作。 默认值为 false
,这意味着缓存清除操作将在被注解的方法成功调用后发生(即,仅在方法调用未抛出异常时进行)。
4.3 key 属性
用于动态计算键的 Spring 表达式语言(SpEL)表达式。
默认值为 ""
,这意味着所有方法参数都被视为键,除非已设置了自定义的 keyGenerator
。
SpEL 表达式会在一个专门的上下文中进行评估,该上下文提供了以下元数据:
#result
用于引用方法调用的结果,但只能在beforeInvocation()
为false
时使用。对于支持的包装器(如Optional
),#result
引用的是实际的对象,而不是包装器。#root.method
、#root.target
和#root.caches
分别引用方法、目标对象和受影响的缓存。- 也有方法名的快捷方式
#root.methodName
和目标类的快捷方式#root.targetClass
。 - 方法参数可以通过索引访问。例如,第二个参数可以通过
#root.args[1]
、#p1
或#a1
访问。如果参数名信息可用,也可以通过名称访问参数。
5、自定义键生成器
需要实现 org.springframework.cache.interceptor.KeyGenerator
接口
四、示例
1、@CacheEvict 注解
@CacheEvict
注解表示一个方法或类上的所有方法调用时,触发缓存清除操作。
触发缓存时,清除哪些缓存是根据注解中的 key
属性来决定的,默认情况下只移除与关联键对应的值。例如,在调用下面的方法时,会根据方法中的键来删除缓存。
1 |
|
对于 allEntries
属性来说,他会删除缓存中所有的条目,这个所有条目的范围可以理解为以 cacheNames
属性为基础,也就是会清除以 cacheNames
前缀的所有元素,而与 key
属性的值没有关系。
另外 allEntries
属性的描述中说明此参数设置为 true
的同时并指定 key
是不允许的。经过代码测试,两者同时存在时,是不会报异常的,只是 key
属性将不起作用。
1 |
|
2、@Cacheable 注解
1 |
|
3、@CachePut 注解
1 |
|
4、关于注解中的 key 属性
key
属性中获取方法入参,可以使用 #root.args[1]
、#p1
或 #a1
来获取。同时也可以使用方法入参来获取。但是在最开始的测试中,看到取到的属性为 null,redis 中查看缓存的键中结尾也是 null。
1 |
|
显示没有获取到方法的键。通过查询可以,可能与代码的编译有关,编译时,没有将具体的方法名编译到字节码中。为了使用方法参数的名称,需要确保编译后的字节码中保留了参数名称的信息。这通常通过在编译时启用 -parameters
选项来实现。
修改 POM 文件之后,rebuild module 之后,就可以正常获取到方法参数值作为键了。
1 | <build> |
5、缓存配置
1 |
|
1 |
|
相关链接
Redis Cache :: Spring Data Redis
OB links
OB tags
#Redis #Spring #未完待续