diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java index 2261848429..a2dace8fc9 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -124,12 +124,14 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { } @Override - public byte[] get(String name, byte[] key) { + public byte[] get(String name, byte[] key, @Nullable Duration ttl) { Assert.notNull(name, "Name must not be null"); Assert.notNull(key, "Key must not be null"); - byte[] result = execute(name, connection -> connection.get(key)); + byte[] result = shouldExpireWithin(ttl) + ? execute(name, connection -> connection.getEx(key, Expiration.from(ttl))) + : execute(name, connection -> connection.get(key)); statistics.incGets(name); diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java index 41982b67b9..59702403d9 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -17,6 +17,7 @@ import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -188,11 +189,21 @@ protected T loadCacheValue(Object key, Callable valueLoader) { @Override protected Object lookup(Object key) { - byte[] value = getCacheWriter().get(getName(), createAndConvertCacheKey(key)); + byte[] value = getCacheConfiguration().isTtiExpirationEnabled() + ? getCacheWriter().get(getName(), createAndConvertCacheKey(key), getTimeToLive(key)) + : getCacheWriter().get(getName(), createAndConvertCacheKey(key)); return value != null ? deserializeCacheValue(value) : null; } + private Duration getTimeToLive(Object key) { + return getTimeToLive(key, null); + } + + private Duration getTimeToLive(Object key, @Nullable Object value) { + return getCacheConfiguration().getTtlFunction().getTimeToLive(key, value); + } + @Override public void put(Object key, @Nullable Object value) { @@ -208,7 +219,7 @@ public void put(Object key, @Nullable Object value) { } getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue), - getCacheConfiguration().getTtlFunction().getTimeToLive(key, value)); + getTimeToLive(key, value)); } @Override @@ -221,7 +232,7 @@ public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { } byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key), - serializeCacheValue(cacheValue), getCacheConfiguration().getTtlFunction().getTimeToLive(key, value)); + serializeCacheValue(cacheValue), getTimeToLive(key, value)); return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null; } diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java index cdae34f7c8..3b43622e62 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java @@ -47,9 +47,11 @@ public class RedisCacheConfiguration { protected static final boolean DEFAULT_CACHE_NULL_VALUES = true; + protected static final boolean DEFAULT_ENABLE_TTI_EXPIRATION = false; protected static final boolean DEFAULT_USE_PREFIX = true; protected static final boolean DO_NOT_CACHE_NULL_VALUES = false; protected static final boolean DO_NOT_USE_PREFIX = false; + protected static final boolean ENABLE_IDLE_TIME_EXPIRATION = true; /** * Default {@link RedisCacheConfiguration} using the following: @@ -108,7 +110,10 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c registerDefaultConverters(conversionService); - return new RedisCacheConfiguration(TtlFunction.persistent(), DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX, + return new RedisCacheConfiguration(TtlFunction.persistent(), + DEFAULT_CACHE_NULL_VALUES, + DEFAULT_ENABLE_TTI_EXPIRATION, + DEFAULT_USE_PREFIX, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), @@ -116,6 +121,7 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c } private final boolean cacheNullValues; + private final boolean enableTtiExpiration; private final boolean usePrefix; private final CacheKeyPrefix keyPrefix; @@ -128,12 +134,13 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c private final TtlFunction ttlFunction; @SuppressWarnings("unchecked") - private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean usePrefix, - CacheKeyPrefix keyPrefix, SerializationPair keySerializationPair, + private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean enableTtiExpiration, + Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair keySerializationPair, SerializationPair valueSerializationPair, ConversionService conversionService) { this.ttlFunction = ttlFunction; this.cacheNullValues = cacheNullValues; + this.enableTtiExpiration = enableTtiExpiration; this.usePrefix = usePrefix; this.keyPrefix = keyPrefix; this.keySerializationPair = keySerializationPair; @@ -168,8 +175,9 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix) Assert.notNull(cacheKeyPrefix, "Function used to compute prefix must not be null"); - return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DEFAULT_USE_PREFIX, - cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(), getConversionService()); + return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(), + DEFAULT_USE_PREFIX, cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(), + getConversionService()); } /** @@ -181,8 +189,9 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix) * @return new {@link RedisCacheConfiguration}. */ public RedisCacheConfiguration disableCachingNullValues() { - return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, usePrefix(), getKeyPrefix(), - getKeySerializationPair(), getValueSerializationPair(), getConversionService()); + return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, isTtiExpirationEnabled(), + usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), + getConversionService()); } /** @@ -193,8 +202,30 @@ public RedisCacheConfiguration disableCachingNullValues() { * @return new {@link RedisCacheConfiguration}. */ public RedisCacheConfiguration disableKeyPrefix() { - return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DO_NOT_USE_PREFIX, - getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService()); + return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(), + DO_NOT_USE_PREFIX, getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService()); + } + + /** + * Enables {@literal time-to-idle (TTI) expiration} on {@link Cache} read operations, + * such as {@link Cache#get(Object)}. + *

+ * Enabling this option applies the same {@link #getTtlFunction() TTL expiration policy} to {@link Cache} read + * operations as it does for {@link Cache} write operations. In effect, this will invoke the Redis {@literal GETEX} + * command in place of {@literal GET}. + *

+ * Redis does not support the concept of {@literal TTI}, only {@literal TTL}. However, if {@literal TTL} expiration + * is applied to all {@link Cache} operations, both read and write alike, and {@link Cache} operations passed with + * expiration are used consistently across the application, then in effect, an application can achieve + * {@literal TTI} expiration-like behavior. + * + * @return this {@link RedisCacheConfiguration}. + * @see GETEX + */ + public RedisCacheConfiguration enableTtiExpiration() { + return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), ENABLE_IDLE_TIME_EXPIRATION, + usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), + getConversionService()); } /** @@ -222,8 +253,9 @@ public RedisCacheConfiguration entryTtl(TtlFunction ttlFunction) { Assert.notNull(ttlFunction, "TtlFunction must not be null"); - return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), usePrefix(), getKeyPrefix(), - getKeySerializationPair(), getValueSerializationPair(), getConversionService()); + return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), isTtiExpirationEnabled(), + usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), + getConversionService()); } /** @@ -236,8 +268,8 @@ public RedisCacheConfiguration serializeKeysWith(SerializationPair keySe Assert.notNull(keySerializationPair, "KeySerializationPair must not be null"); - return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(), - keySerializationPair, getValueSerializationPair(), getConversionService()); + return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(), + usePrefix(), getKeyPrefix(), keySerializationPair, getValueSerializationPair(), getConversionService()); } /** @@ -250,8 +282,8 @@ public RedisCacheConfiguration serializeValuesWith(SerializationPair valueSer Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null"); - return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(), - getKeySerializationPair(), valueSerializationPair, getConversionService()); + return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(), + usePrefix(), getKeyPrefix(), getKeySerializationPair(), valueSerializationPair, getConversionService()); } /** @@ -264,8 +296,8 @@ public RedisCacheConfiguration withConversionService(ConversionService conversio Assert.notNull(conversionService, "ConversionService must not be null"); - return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(), - getKeySerializationPair(), getValueSerializationPair(), conversionService); + return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(), + usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), conversionService); } /** @@ -275,6 +307,19 @@ public boolean getAllowCacheNullValues() { return this.cacheNullValues; } + /** + * Determines whether {@literal time-to-idle (TTI) expiration} has been enabled for caching. + *

+ * Use {@link #enableTtiExpiration()} to opt-in and enable {@literal time-to-idle (TTI) expiration} for caching. + * + * @return {@literal true} if {@literal time-to-idle (TTI) expiration} was configured and enabled for caching. + * Defaults to {@literal false}. + * @see GETEX + */ + public boolean isTtiExpirationEnabled() { + return this.enableTtiExpiration; + } + /** * @return {@literal true} if cache keys need to be prefixed with the {@link #getKeyPrefixFor(String)} if present or * the default which resolves to {@link Cache#getName()}. diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java index fb1adc6e46..4c54186e21 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -85,7 +85,9 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio */ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory, BatchStrategy batchStrategy) { - return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(), batchStrategy); + + return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(), + batchStrategy); } /** @@ -104,29 +106,43 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction, CacheStatisticsCollector.none(), - batchStrategy); + return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction, + CacheStatisticsCollector.none(), batchStrategy); } /** - * Write the given key/value pair to Redis and set the expiration time if defined. + * Get the binary value representation from Redis stored for the given key. * - * @param name The cache name must not be {@literal null}. - * @param key The key for the cache entry. Must not be {@literal null}. - * @param value The value stored for the key. Must not be {@literal null}. - * @param ttl Optional expiration time. Can be {@literal null}. + * @param name must not be {@literal null}. + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist. + * @see #get(String, byte[], Duration) */ - void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); + @Nullable + default byte[] get(String name, byte[] key) { + return get(name, key, null); + } /** - * Get the binary value representation from Redis stored for the given key. + * Get the binary value representation from Redis stored for the given key and set the given + * {@link Duration TTL expiration} for the cache entry. * * @param name must not be {@literal null}. * @param key must not be {@literal null}. - * @return {@literal null} if key does not exist. + * @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry. + * @return {@literal null} if key does not exist or has {@literal expired}. */ - @Nullable - byte[] get(String name, byte[] key); + byte[] get(String name, byte[] key, @Nullable Duration ttl); + + /** + * Write the given key/value pair to Redis and set the expiration time if defined. + * + * @param name The cache name must not be {@literal null}. + * @param key The key for the cache entry. Must not be {@literal null}. + * @param value The value stored for the key. Must not be {@literal null}. + * @param ttl Optional expiration time. Can be {@literal null}. + */ + void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); /** * Write the given value to Redis if the key does not already exist. diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index 326fc78a37..aa275a105f 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -455,13 +455,14 @@ void multipleThreadsLoadValueOnce() throws InterruptedException { AtomicReference storage = new AtomicReference<>(); cache = new RedisCache("foo", new RedisCacheWriter() { + @Override public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { storage.set(value); } @Override - public byte[] get(String name, byte[] key) { + public byte[] get(String name, byte[] key, @Nullable Duration ttl) { prepare.countDown(); try {