Redis 缓存

Spring Data Redis 在 org.springframework.data.redis.cache 包中提供了 Spring Framework 的 缓存抽象 的实现。 要将 Redis 用作后端实现,请将 RedisCacheManager 添加到您的配置中,如下所示:spring-doc.cadn.net.cn

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

RedisCacheManager 行为可以通过 RedisCacheManager.RedisCacheManagerBuilder 进行配置,允许您设置默认的 RedisCacheManager、事务行为和预定义缓存。spring-doc.cadn.net.cn

RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    .transactionAware()
    .withInitialCacheConfigurations(Collections.singletonMap("predefined",
        RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
    .build();

如前例所示,RedisCacheManager 允许对每个缓存进行自定义配置。spring-doc.cadn.net.cn

RedisCacheManager创建的RedisCache的行为通过RedisCacheConfiguration定义。 配置允许您设置键的过期时间、前缀以及用于与二进制存储格式进行转换的RedisSerializer实现,如下示例所示:spring-doc.cadn.net.cn

RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofSeconds(1))
    .disableCachingNullValues();

RedisCacheManager 默认使用无锁的 RedisCacheWriter 来读写二进制值。 无锁缓存可提高吞吐量。 由于缺乏条目锁定,可能导致 CacheputIfAbsentclean 操作的命令重叠且非原子执行,因为这些操作需要向 Redis 发送多个命令。 而带锁的对应实现通过设置显式锁键并检查该键是否存在来防止命令重叠,但这会导致额外的请求以及潜在的命令等待时间。spring-doc.cadn.net.cn

锁定作用于缓存级别,而不是每个缓存条目spring-doc.cadn.net.cn

可以按如下方式选择启用锁定行为:spring-doc.cadn.net.cn

RedisCacheManager cacheManager = RedisCacheManager
    .builder(RedisCacheWriter.lockingRedisCacheWriter(connectionFactory))
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    ...

默认情况下,缓存条目的任何 key 都会以实际的缓存名称加上两个冒号(::)作为前缀。 此行为可以更改为使用静态前缀或计算得出的前缀。spring-doc.cadn.net.cn

以下示例展示了如何设置一个静态前缀:spring-doc.cadn.net.cn

// static key prefix
RedisCacheConfiguration.defaultCacheConfig().prefixCacheNameWith("(͡° ᴥ ͡°)");

The following example shows how to set a computed prefix:

// computed key prefix
RedisCacheConfiguration.defaultCacheConfig()
    .computePrefixWith(cacheName -> "¯\_(ツ)_/¯" + cacheName);

缓存实现默认使用 KEYSDEL 来清除缓存。KEYS 在键空间较大时可能导致性能问题。 因此,可以使用 RedisCacheWriter 创建默认的 BatchStrategy,以切换到基于 SCAN 的批处理策略。 SCAN 策略需要指定一个批处理大小,以避免过多的 Redis 命令往返:spring-doc.cadn.net.cn

RedisCacheManager cacheManager = RedisCacheManager
    .builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
    .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
    ...

使用任意驱动程序和 Redis 操作模式(独立模式、集群模式)时,KEYS 批处理策略均得到完全支持。 使用 Lettuce 驱动程序时,SCAN 得到完全支持。 Jedis 仅在非集群模式下支持 SCANspring-doc.cadn.net.cn

下表列出了 RedisCacheManager 的默认设置:spring-doc.cadn.net.cn

表1。RedisCacheManager 默认值
设置

缓存写入器spring-doc.cadn.net.cn

非锁定的 KEYS 批处理策略spring-doc.cadn.net.cn

缓存配置spring-doc.cadn.net.cn

RedisCacheConfiguration#defaultConfigurationspring-doc.cadn.net.cn

初始缓存spring-doc.cadn.net.cn

spring-doc.cadn.net.cn

事务感知spring-doc.cadn.net.cn

Nospring-doc.cadn.net.cn

下表列出了 RedisCacheConfiguration 的默认设置:spring-doc.cadn.net.cn

表2. RedisCacheConfiguration 默认值
键过期

缓存 nullspring-doc.cadn.net.cn

是的spring-doc.cadn.net.cn

前缀键spring-doc.cadn.net.cn

是的spring-doc.cadn.net.cn

默认前缀spring-doc.cadn.net.cn

实际的缓存名称spring-doc.cadn.net.cn

键序列化器spring-doc.cadn.net.cn

StringRedisSerializerspring-doc.cadn.net.cn

值序列化器spring-doc.cadn.net.cn

JdkSerializationRedisSerializerspring-doc.cadn.net.cn

转换服务spring-doc.cadn.net.cn

带有默认缓存键转换器的DefaultFormattingConversionServicespring-doc.cadn.net.cn

默认情况下,RedisCache 的统计信息是禁用的。 使用 RedisCacheManagerBuilder.enableStatistics() 可通过 RedisCache#getStatistics() 收集本地的命中未命中数据,并返回所收集数据的快照。spring-doc.cadn.net.cn

Redis 缓存过期

空闲时间(TTI)和生存时间(TTL)的实现,即使在不同的数据存储之间,其定义和行为也各不相同。spring-doc.cadn.net.cn

一般情况下:spring-doc.cadn.net.cn

  • 生存时间(TTL)过期策略 - TTL 仅在执行创建或更新数据访问操作时被设置或重置。 只要在 TTL 过期超时之前对条目进行了写入操作(包括创建时),该条目的超时时间就会重置为所配置的 TTL 过期超时时长。 例如,如果 TTL 过期超时设置为 5 分钟,那么在创建条目时,其超时时间将被设为 5 分钟;此后,只要在 5 分钟间隔到期前对该条目进行更新,其超时时间就会再次重置为 5 分钟。 如果在 5 分钟内未发生任何更新操作,即使该条目在此期间被读取多次,甚至仅被读取一次,该条目仍然会过期。 在声明 TTL 过期策略时,必须对条目执行写入操作,才能防止其过期。spring-doc.cadn.net.cn

  • 空闲时间(TTI)过期 - 每当条目被读取或更新时,TTI 都会被重置,这实际上是对 TTL 过期策略的一种扩展。spring-doc.cadn.net.cn

某些数据存储在配置了 TTL(生存时间)后,无论对该条目执行何种类型的数据访问操作(读取、写入或其他操作),都会在 TTL 到期时使该条目失效。 一旦达到所设置的 TTL 过期时限,该条目将无论如何都会从数据存储中被逐出。 逐出操作(例如:销毁、失效、溢写到磁盘(针对持久化存储)等)具体取决于数据存储的实现。spring-doc.cadn.net.cn

生存时间 (TTL) 过期

Spring Data Redis 的 Cache 实现支持缓存条目的生存时间(TTL)过期机制。 用户可以通过配置固定的 Duration 来设置 TTL 过期时间,也可以通过提供新的 Duration 接口的实现,为每个缓存条目动态计算 RedisCacheWriter.TtlFunctionspring-doc.cadn.net.cn

RedisCacheWriter.TtlFunction 接口是在 Spring Data Redis 3.2.0 中引入的。spring-doc.cadn.net.cn

如果所有缓存条目都应在一段固定时间后过期,则只需配置一个带有固定 Duration 的 TTL(生存时间)过期超时,如下所示:spring-doc.cadn.net.cn

RedisCacheConfiguration fiveMinuteTtlExpirationDefaults =
    RedisCacheConfiguration.defaultCacheConfig().enableTtl(Duration.ofMinutes(5));

然而,如果缓存条目的 TTL(生存时间)过期超时需要各不相同,则必须提供 RedisCacheWriter.TtlFunction 接口的自定义实现:spring-doc.cadn.net.cn

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 实现中,该实现返回所提供的 Durationspring-doc.cadn.net.cn

然后,你可以通过以下方式在全局范围内配置固定的 Duration,或为每个缓存条目动态设置 Duration 的 TTL(生存时间)过期策略:spring-doc.cadn.net.cn

全局固定的 Duration TTL 过期超时时间
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
    .cacheDefaults(fiveMinuteTtlExpirationDefaults)
    .build();

或者,也可以这样:spring-doc.cadn.net.cn

全局、动态计算的每个缓存条目的持续时间(TTL)过期超时
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(MyCustomTtlFunction.INSTANCE);

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

当然,你可以通过以下方式结合使用全局配置和每个缓存的配置:spring-doc.cadn.net.cn

全局固定的 Duration TTL 过期超时时间
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 本身并不支持真正的空闲时间(Time-to-Idle,TTI)过期机制。 然而,通过使用 Spring Data Redis 的缓存实现,可以实现类似空闲时间(TTI)过期的行为。spring-doc.cadn.net.cn

在 Spring Data Redis 的缓存实现中,TTI(空闲时间)配置必须显式启用,即采用“选择加入”(opt-in)的方式。 此外,您还必须提供 TTL(生存时间)配置,可以使用固定的 Duration,也可以使用如上文Redis 缓存过期所述的 #redis:support:cache-abstraction:expiration 接口的自定义实现。spring-doc.cadn.net.cn

@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(空闲时间)概念,因此只有通过支持设置过期选项的 Redis 命令才能实现 TTI。 在 Redis 中,“过期”在技术上是一种生存时间(TTL)策略。 然而,在读取某个键的值时可以传入 TTL 过期参数,从而有效地重置 TTL 过期超时时间,Spring Data Redis 的 Cache.get(key) 操作现在正是这样做的。spring-doc.cadn.net.cn

RedisCache.get(key) 是通过调用 Redis 的 GETEX 命令实现的。spring-doc.cadn.net.cn

Redis GETEX 命令仅在 Redis 6.2.0 及更高版本中可用。 因此,如果您未使用 Redis 6.2.0 或更高版本,则无法使用 Spring Data Redis 的 TTI 过期功能。 如果在不兼容的 Redis(服务器)版本上启用 TTI,将抛出命令执行异常。 系统不会尝试判断 Redis 服务器版本是否正确或是否支持 GETEX 命令。spring-doc.cadn.net.cn

为了在您的 Spring Data Redis 应用程序中实现真正的空闲时间(TTI)过期行为,每次读取或写入操作都必须始终以设置生存时间(TTL)的方式访问条目。此规则没有例外。如果你在 Spring Data Redis 应用程序中混合使用了不同的数据访问模式(例如:缓存、使用 RedisTemplate 调用操作,以及可能或尤其在使用 Spring Data Repository 的 CRUD 操作时),那么即使访问了某个条目,也不一定会阻止该条目因设置了 TTL(生存时间)而过期。例如,在一个@Cacheable服务方法调用期间,条目可能会被“写入”(存入)缓存,并带有TTL过期时间。e.SET <expiration options>) 和后续版本可以通过一个 Spring Data Redis Repository 在过期超时前读取(使用 GET 而不带过期选项)。一个简单的GET不指定过期选项将不会重置条目的TTL过期超时。因此,即使该条目刚刚被读取过,它也可能在下一次数据访问操作之前就已过期。由于 Redis 服务器无法强制执行此操作,因此当配置了空闲时间(time-to-idle)过期策略时,您的应用程序有责任在缓存内外始终如一地访问该条目(在适当的情况下)。spring-doc.cadn.net.cn