Redis Cache

Spring Data Redis 在 org.springframework.data.redis.cache 包中提供了 Spring 框架 {spring-framework-docs}/integration.html#cache[缓存抽象] 的实现。要将 Redis 用作支持实现,请将 RedisCacheManager 添加到您的配置中,如下所示:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    return RedisCacheManager.create(connectionFactory);
}

可以使用“RedisCacheManagerBuilder”配置“RedisCacheManager”行为,让您设置默认的“RedisCacheConfiguration”、“transaction”行为和预定义缓存。

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 cacheMangager = RedisCacheManager
    .build(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory))
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    ...

默认情况下,缓存条目的任何“密钥”都以实际缓存名称开头,后跟两个冒号(“::”)。此行为可以更改为静态前缀以及计算前缀。 以下示例演示如何设置静态前缀:

// 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
    .build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    ...

“KEYS”批处理策略完全支持使用任何驱动程序和 Redis 操作模式(独立、群集)。当使用 Lettuce 驱动程序时,完全支持“SCAN”。Jedis 仅在非群集模式下支持“SCAN”。

下表列出了“RedisCacheManager”的默认设置:

Table 1. RedisCacheManager defaults
Setting Value

Cache Writer

Non-locking, KEYS batch strategy

Cache Configuration

RedisCacheConfiguration#defaultConfiguration

Initial Caches

None

Transaction Aware

No

下表列出了“RedisCacheConfiguration”的默认设置:

Table 2. RedisCacheConfiguration defaults
Key Expiration None

Cache null

Yes

Prefix Keys

Yes

Default Prefix

The actual cache name

Key Serializer

StringRedisSerializer

Value Serializer

JdkSerializationRedisSerializer

Conversion Service

DefaultFormattingConversionService 并默认缓存密钥转换器

默认情况下,禁用“RedisCache”统计信息。使用“RedisCacheManagerBuilder.enableStatistics()”通过“RedisCache#getStatistics()”收集本地命中和未命中,返回收集的数据的快照。

Redis Cache Expiration

即使在不同的数据存储之间,空闲时间 (TTI) 以及生存时间 (TTL) 的实现也在定义和行为上有所不同。

一般情况下:

  • time-to-live (TTL) expiration - TTL 仅通过创建或更新数据访问操作设置和重置。只要在 TTL 过期超时之前写入该项,包括在创建时,该项的超时将重置为 TTL 过期超时的配置持续时间。例如,如果 TTL 过期超时设置为 5 分钟,那么该超时将在条目创建时设置为 5 分钟,并在随后的 5 分钟间隔期内每次更新条目时重置为 5 分钟。如果在 5 分钟内未更新,即使在 5 分钟间隔内多次读取或甚至只读取一次,该条目仍然会过期。必须写入条目以防止在声明 TTL 过期策略时条目过期。

  • time-to-idle (TTI) expiration - 每次读取条目以及更新条目时,TTI 都会重置,并且实际上是 TTL 过期策略的扩展。

某些数据存储会在配置 TTL 时使条目过期,无论在该条目上发生何种类型的数据访问操作(读取、写入或其他)。在设置配置的 TTL 过期超时后,不管怎样,该条目都会从数据存储中驱逐。驱逐操作(例如:销毁、使无效、溢出到磁盘(对于持久存储)等)特定于数据存储。

Time-To-Live (TTL) Expiration

Spring Data Redis 的“Cache”实现支持缓存条目上的生存时间 (TTL) 过期。用户可以通过一个固定的“持续时间”或通过提供新的“RedisCacheWriter.TtlFunction”界面的实现为每个缓存条目配置动态计算的“持续时间”来配置 TTL 过期超时。

“RedisCacheWriter.TtlFunction”界面是在 Spring Data Redis“3.2.0”中引入的。

如果所有缓存条目都应在设定的持续时间后过期,那么只需使用一个固定的“持续时间”配置 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
    }
}

计算下,一个固定的“持续时间”TTL 过期将包装在“TtlFunction”实现中,返回提供的“持续时间”。

然后,你可以使用以下方式在全局基础上配置固定的“持续时间”或动态的按缓存条目“持续时间”TTL 过期:

Global fixed Duration TTL expiration timeout
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
    .cacheDefaults(fiveMinuteTtlExpirationDefaults)
    .build();

或者,也可以选择以下方式:

Global, dynamically computed per-cache entry Duration TTL expiration timeout
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(MyCustomTtlFunction.INSTANCE);

RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
    .cacheDefaults(defaults)
    .build();

当然,你可以使用以下方式组合全局和按缓存配置:

Global fixed Duration TTL expiration timeout
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();

Time-To-Idle (TTI) Expiration

Redis 本身不支持真实空闲时间 (TTI) 过期的概念。不过,使用 Spring Data Redis 的 Cache 实现,可以实现类似空闲时间 (TTI) 过期行为。

Spring Data Redis 缓存实现中的 TTI 配置必须显式启用,即为选择加入。此外,你还必须使用固定的 DurationTtlFunction 接口的自定义实现来提供 TTL 配置,如前文 [Redis 缓存到期时间,redis:support:cache-abstraction:expiration] 所述。

例如:

@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 到期时间)。例如,在 @Cacheable 服务方法调用期间,一个条目可能会“被放入”(写入)缓存,并带有 TTL 到期时间(即 SET <expiration options>),然后在到期时间超时之前使用 Spring Data Redis Repository 读取(使用 GET,不带到期选项)。不指定到期选项的简单 GET 不会重置条目上的 TTL 到期时间超时。因此,此条目在下次数据访问操作之前可能过期,即使它刚刚被读取过。由于无法在 Redis 服务器中强制执行此项操作,因此如果配置了空闲时间到期时间,则你的应用程序有责任在缓存内外(视情况而定)始终访问一个条目。