Redis 缓存
Spring Data Redis 在 org.springframework.data.redis.cache 包中提供了 Spring Framework 缓存抽象 的一个实现。要把 Redis 用作后端实现,请按如下方式把 RedisCacheManager 添加到你的配置中:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.create(connectionFactory);
}
可以用 RedisCacheManager.RedisCacheManagerBuilder 配置 RedisCacheManager 的行为,让你设置默认的 RedisCacheManager、事务行为以及预定义缓存。
RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
.transactionAware()
.withInitialCacheConfigurations(Collections.singletonMap("predefined",
RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
.build();
如上例所示,RedisCacheManager 允许按缓存进行自定义配置。
由 RedisCacheManager 创建的 RedisCache 的行为由 RedisCacheConfiguration 定义。该配置让你可以设置键的过期时间、前缀以及用于与二进制存储格式相互转换的 RedisSerializer 实现,如下例所示:
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(1))
.disableCachingNullValues();
RedisCacheManager 默认使用一个无锁的 RedisCacheWriter 来读写二进制值。无锁缓存可提高吞吐量。缺少条目锁定可能导致 Cache 的 putIfAbsent 和 clean 操作出现重叠的、非原子的命令,因为这些操作需要向 Redis 发送多条命令。带锁的对应实现通过设置一个显式锁键并检查该键是否存在来防止命令重叠,这会带来额外的请求和潜在的命令等待时间。
锁加在缓存级别,而不是按缓存条目加锁。
可以按如下方式选择启用锁定行为:
RedisCacheManager cacheManager = RedisCacheManager
.builder(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory))
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
...
默认情况下,缓存条目的任何 key 都会被加上实际缓存名后接两个冒号(::)作为前缀。可以把这一行为改为静态前缀,也可以改为计算得出的前缀。
下例展示了如何设置静态前缀:
// static key prefix
RedisCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)");
The following example shows how to set a computed prefix:
// computed key prefix
RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName);
缓存实现默认使用 KEYS 和 DEL 来清空缓存。对于大型键空间,KEYS 可能导致性能问题。因此,默认的 RedisCacheWriter 可以用一个 BatchStrategy 创建,以切换为基于 SCAN 的批量策略。SCAN 策略需要指定批量大小,以避免过多的 Redis 命令往返:
RedisCacheManager cacheManager = RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
...
KEYS 批量策略在使用任何驱动和任何 Redis 操作模式(Standalone、Clustered)时都完全受支持。使用 Lettuce 驱动时 SCAN 完全受支持。Jedis 仅在非集群模式下支持 SCAN。
下表列出了 RedisCacheManager 的默认设置:
表 1. RedisCacheManager 默认值
| 设置 | 值 |
|---|---|
| 缓存写入器 | 非锁定,KEYS 批量策略 |
| 缓存配置 | RedisCacheConfiguration#defaultConfiguration |
| 初始缓存 | 无 |
| 事务感知 | 否 |
下表列出了 RedisCacheConfiguration 的默认设置:
表 2. RedisCacheConfiguration 默认值
| 键过期 | 无 |
|---|---|
缓存 null | 是 |
| 前缀键 | 是 |
| 默认前缀 | 实际的缓存名 |
| Key 序列化器 | StringRedisSerializer |
| Value 序列化器 | JdkSerializationRedisSerializer |
| 转换服务 | DefaultFormattingConversionService,使用默认缓存键转换器 |
默认情况下 RedisCache 的统计是禁用的。使用 RedisCacheManagerBuilder.enableStatistics() 可以通过 RedisCache#getStatistics() 收集本地的_命中_和_未命中_,返回所收集数据的快照。
Redis 缓存过期 {#redis:support:cache-abstraction:expiration}
空闲时间(TTI)以及存活时间(TTL)的实现在定义和行为上即使在不同数据存储之间也各不相同。
一般而言:
- 存活时间(TTL)过期 — TTL 只由创建或更新数据访问操作设置和重置。只要条目在 TTL 过期超时之前被写入(包括创建时),条目的超时就会重置为 TTL 过期超时的配置时长。例如,如果 TTL 过期超时设为 5 分钟,那么条目创建时超时会设为 5 分钟,此后在该条目每次被更新时、以及 5 分钟间隔到期之前都会重置为 5 分钟。如果在 5 分钟内没有发生更新,即使该条目被读取了好几次,甚至只是在 5 分钟间隔内读取过一次,该条目仍会过期。声明 TTL 过期策略时,必须写入条目才能防止条目过期。
- 空闲时间(TTI)过期 — 无论是读取条目还是更新条目,TTI 都会被重置,它实际上是 TTL 过期策略的一种扩展。
某些数据存储在配置了 TTL 后,无论对条目发生哪种类型的数据访问操作(读、写或其他),都会让条目过期。在设定的 TTL 过期超时之后,条目会被无条件地从数据存储中驱逐。驱逐动作(例如:销毁、失效、溢出到磁盘(对于持久化存储)等)是数据存储特定的。
存活时间(TTL)过期 {#redis:support:cache-abstraction:expiration:tti}
Spring Data Redis 的 Cache 实现支持对缓存条目的_存活时间_(TTL)过期。用户可以为每个缓存条目配置固定的 Duration TTL 过期超时,也可以通过提供新的 RedisCacheWriter.TtlFunction 接口的实现来动态计算 Duration。
RedisCacheWriter.TtlFunction 接口是在 Spring Data Redis 3.2.0 中引入的。
如果希望所有缓存条目都在设定的时长后过期,那么只需配置一个固定 Duration 的 TTL 过期超时,如下所示:
RedisCacheConfiguration fiveMinuteTtlExpirationDefaults =
RedisCacheConfiguration.defaultCacheConfig().enableTtl(Duration.ofMinutes(5));
但是,如果 TTL 过期超时应随缓存条目而变化,则必须提供 RedisCacheWriter.TtlFunction 接口的自定义实现:
enum MyCustomTtlFunction implements TtlFunction {
INSTANCE;
@Override
public Duration getTimeToLive(Object key, @Nullable Object value) {
// compute a TTL expiration timeout (Duration) based on the cache entry key and/or value
}
}
在底层,固定 Duration 的 TTL 过期会被包装在一个返回所提供 Duration 的 TtlFunction 实现中。
然后,你可以使用以下方式在全局层面配置固定 Duration,或按缓存条目动态配置 Duration 的 TTL 过期:
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(fiveMinuteTtlExpirationDefaults)
.build();
或者,也可以:
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(MyCustomTtlFunction.INSTANCE);
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaults)
.build();
当然,你可以把全局配置和按缓存配置结合起来使用:
RedisCacheConfiguration predefined = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(MyCustomTtlFunction.INSTANCE);
Map<String, RedisCacheConfiguration> initialCaches = Collections.singletonMap("predefined", predefined);
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(fiveMinuteTtlExpirationDefaults)
.withInitialCacheConfigurations(initialCaches)
.build();
空闲时间(TTI)过期 {#redis:support:cache-abstraction:expiration:tti2}
Redis 本身并不支持真正的空闲时间(TTI)过期这一概念。不过,使用 Spring Data Redis 的 Cache 实现,仍然可以实现类似空闲时间(TTI)过期的行为。
Spring Data Redis 的 Cache 实现中,TTI 的配置必须显式启用,也就是说它是 opt-in 的。此外,你还必须像前面Redis 缓存过期中所述那样,使用固定 Duration 或 TtlFunction 接口的自定义实现来提供 TTL 配置。
例如:
@Configuration
@EnableCaching
class RedisConfiguration {
@Bean
RedisConnectionFactory redisConnectionFactory() {
// ...
}
@Bean
RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.enableTimeToIdle();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaults)
.build();
}
}
由于 Redis 服务器并未实现真正的 TTI 概念,因此 TTI 只能通过接受过期选项的 Redis 命令来实现。在 Redis 中,“过期”在技术上是一种存活时间(TTL)策略。不过,在读取键的值时可以传入 TTL 过期,从而有效地重置 TTL 过期超时,Spring Data Redis 的 Cache.get(key) 操作现在就是这种情况。
RedisCache.get(key) 通过调用 Redis 的 GETEX 命令来实现。
Redis 的 GETEX 命令仅在 Redis 6.2.0 及更高版本中可用。因此,如果你使用的不是 Redis 6.2.0 或更高版本,就无法使用 Spring Data Redis 的 TTI 过期。如果在不兼容的 Redis(服务器)版本上启用 TTI,会抛出命令执行异常。系统不会尝试判断 Redis 服务器版本是否正确、是否支持 GETEX 命令。
为了在你的 Spring Data Redis 应用中实现真正的空闲时间(TTI)过期般的行为,必须在每次读或写操作时都一致地对条目施加(TTL)过期。这条规则没有例外。如果你在 Spring Data Redis 应用中混用不同的数据访问模式(例如:缓存、使用 RedisTemplate 调用操作,或者尤其是使用 Spring Data Repository CRUD 操作),那么访问一个条目不一定能在设置了 TTL 过期时阻止该条目过期。例如,一个条目可能在带 TTL 过期的 @Cacheable 服务方法调用期间被“放入”(写入)缓存(即 SET <expiration options>),随后在过期超时之前用 Spring Data Redis Repository 读取(使用不带过期选项的 GET)。不指定过期选项的简单 GET 不会重置条目的 TTL 过期超时。因此,即使该条目刚被读取过,也可能在下一次数据访问操作之前过期。由于这无法在 Redis 服务器端强制执行,因此在配置了空闲时间过期时,由你的应用负责在合适的时机、在缓存内外一致地访问条目。