Redis 缓存
Spring Data Redis 在 org.springframework.data.redis.cache
包中提供了 Spring 框架的缓存抽象的实现。要将 Redis 作为后备实现,请将 RedisCacheManager 添加到您的配置中,如下所示:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.create(connectionFactory);
}
RedisCacheManager
的行为可以通过 RedisCacheManager.RedisCacheManagerBuilder 进行配置,允许你设置默认的 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 操作模式(单机模式、集群模式)时都完全支持。使用 Lettuce 驱动程序时,SCAN
完全支持。Jedis 仅在非集群模式下支持 SCAN
。
下表列出了 RedisCacheManager
的默认设置:
表 1. RedisCacheManager
默认值
设置项 | 值 |
---|---|
缓存写入器 | 非锁定,KEYS 批量策略 |
缓存配置 | RedisCacheConfiguration#defaultConfiguration |
初始缓存 | 无 |
事务感知 | 否 |
下表列出了 RedisCacheConfiguration
的默认设置:
表 2. RedisCacheConfiguration 默认配置
键过期时间 | 无 |
---|---|
缓存 null | 是 |
前缀键 | 是 |
默认前缀 | 实际的缓存名称 |
键序列化器 | StringRedisSerializer |
值序列化器 | 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 过期时间被包装在一个 TtlFunction
实现中,该实现返回提供的 Duration
。
然后,你可以使用以下方式全局配置固定的 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 本身并不支持真正的空闲时间(Time-to-Idle, TTI)过期概念。不过,通过使用 Spring Data Redis 的缓存实现,可以实现类似于空闲时间(TTI)过期的行为。
在 Spring Data Redis 的缓存实现中,TTI(Time To Idle)的配置必须显式启用,即需要手动开启。此外,你还必须提供 TTL(Time To Live)配置,可以使用固定的 Duration
或如上文 Redis 缓存过期 中所述的自定义 TtlFunction
接口实现。
例如:
@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(Time-To-Idle)概念,因此只能通过支持过期选项的 Redis 命令来实现 TTI。在 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 应用程序中实现真正的空闲时间失效(Time-to-Idle, TTI)行为,必须在每次读取或写入操作时以生存时间(Time-to-Live, TTL)失效的方式一致地访问条目。此规则没有例外。如果在 Spring Data Redis 应用程序中混合使用了不同的数据访问模式(例如:缓存、使用 RedisTemplate
调用操作,或者特别是在使用 Spring Data Repository 的 CRUD 操作时),则访问条目可能无法阻止其失效(如果设置了 TTL 失效)。例如,可能在 @Cacheable
服务方法调用期间将条目“放入”(写入)缓存,并设置了 TTL 失效(即 SET <expiration options>
),然后在失效超时之前使用 Spring Data Redis Repository 读取该条目(使用不带失效选项的 GET
)。简单的 GET
操作(未指定失效选项)不会重置条目的 TTL 失效超时。因此,即使刚刚读取了条目,它也可能在下一次数据访问操作之前失效。由于 Redis 服务器无法强制执行此行为,因此在配置了空闲时间失效的情况下,应用程序有责任在适当的情况下(无论是否在缓存中)一致地访问条目。