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 1252305a6b..6c511339ce 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -44,6 +44,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author André Prata + * @author Shyngys Sapraliyev * @since 2.0 */ class DefaultRedisCacheWriter implements RedisCacheWriter { @@ -93,7 +94,7 @@ class DefaultRedisCacheWriter implements RedisCacheWriter { } @Override - public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl, @Nullable Duration maxIdle) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(key, "Key must not be null!"); @@ -104,7 +105,11 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { if (shouldExpireWithin(ttl)) { connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert()); } else { - connection.set(key, value); + if (shouldExpireMaxIdleWithin(maxIdle)) { + connection.set(key, value, Expiration.from(maxIdle), SetOption.upsert()); + } else { + connection.set(key, value); + } } return "OK"; @@ -114,12 +119,18 @@ 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 maxIdle) { 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; + + if (shouldExpireMaxIdleWithin(maxIdle)) { + result = execute(name, connection -> connection.getEx(key, Expiration.from(maxIdle))); + } else { + result = execute(name, connection -> connection.get(key)); + } statistics.incGets(name); @@ -133,7 +144,7 @@ public byte[] get(String name, byte[] key) { } @Override - public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl, @Nullable Duration maxIdle) { Assert.notNull(name, "Name must not be null!"); Assert.notNull(key, "Key must not be null!"); @@ -152,7 +163,11 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat if (shouldExpireWithin(ttl)) { put = connection.set(key, value, Expiration.from(ttl), SetOption.ifAbsent()); } else { - put = connection.setNX(key, value); + if (shouldExpireMaxIdleWithin(maxIdle)) { + put = connection.set(key, value, Expiration.from(maxIdle), SetOption.ifAbsent()); + } else { + put = connection.setNX(key, value); + } } if (put) { @@ -318,6 +333,10 @@ private static boolean shouldExpireWithin(@Nullable Duration ttl) { return ttl != null && !ttl.isZero() && !ttl.isNegative(); } + private static boolean shouldExpireMaxIdleWithin(@Nullable Duration maxIdle) { + return maxIdle != null && !maxIdle.isZero() && !maxIdle.isNegative(); + } + private static byte[] createCacheLockKey(String name) { return (name + "~lock").getBytes(StandardCharsets.UTF_8); } 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 17cb9d2f15..12942e7fe2 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java @@ -45,6 +45,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Piotr Mionskowski + * @author Shyngys Sapraliyev * @see RedisCacheConfiguration * @see RedisCacheWriter * @since 2.0 @@ -82,7 +83,7 @@ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfig @Override protected Object lookup(Object key) { - byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key)); + byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key), cacheConfig.getMaxIdle()); if (value == null) { return null; @@ -145,7 +146,8 @@ public void put(Object key, @Nullable Object value) { name)); } - cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl()); + cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), + cacheConfig.getTtl(), cacheConfig.getMaxIdle()); } @Override @@ -158,7 +160,7 @@ public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { } byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), - cacheConfig.getTtl()); + cacheConfig.getTtl(), cacheConfig.getMaxIdle()); if (result == null) { return 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 07bc8190d9..635f0726b9 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java @@ -38,11 +38,13 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Shyngys Sapraliyev * @since 2.0 */ public class RedisCacheConfiguration { private final Duration ttl; + private final Duration maxIdle; private final boolean cacheNullValues; private final CacheKeyPrefix keyPrefix; private final boolean usePrefix; @@ -53,10 +55,11 @@ public class RedisCacheConfiguration { private final ConversionService conversionService; @SuppressWarnings("unchecked") - private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, + private RedisCacheConfiguration(Duration ttl, Duration maxIdle, + Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair keySerializationPair, SerializationPair valueSerializationPair, ConversionService conversionService) { - + this.maxIdle = maxIdle; this.ttl = ttl; this.cacheNullValues = cacheNullValues; this.usePrefix = usePrefix; @@ -123,8 +126,8 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c registerDefaultConverters(conversionService); - return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), - SerializationPair.fromSerializer(RedisSerializer.string()), + return new RedisCacheConfiguration(Duration.ZERO, Duration.ZERO, true, true, + CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService); } @@ -138,8 +141,23 @@ public RedisCacheConfiguration entryTtl(Duration ttl) { Assert.notNull(ttl, "TTL duration must not be null!"); - return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, usePrefix, keyPrefix, + keySerializationPair, valueSerializationPair, conversionService); + } + + /** + * Set the ttl to apply for cache entries on fetch + * Using with {@link Duration#ZERO} will not affect cache entries + * + * @param maxIdle must not be {@literal null}. + * @return new {@link RedisCacheConfiguration}. + */ + public RedisCacheConfiguration maxIdle(Duration maxIdle) { + + Assert.notNull(maxIdle, "maxIdle duration must not be null!"); + + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, usePrefix, keyPrefix, + keySerializationPair, valueSerializationPair, conversionService); } /** @@ -169,8 +187,8 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix) Assert.notNull(cacheKeyPrefix, "Function for computing prefix must not be null!"); - return new RedisCacheConfiguration(ttl, cacheNullValues, true, cacheKeyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, true, cacheKeyPrefix, + keySerializationPair, valueSerializationPair, conversionService); } /** @@ -182,8 +200,8 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix) * @return new {@link RedisCacheConfiguration}. */ public RedisCacheConfiguration disableCachingNullValues() { - return new RedisCacheConfiguration(ttl, false, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair, - conversionService); + return new RedisCacheConfiguration(ttl, maxIdle, false, usePrefix, keyPrefix, + keySerializationPair, valueSerializationPair, conversionService); } /** @@ -195,8 +213,8 @@ public RedisCacheConfiguration disableCachingNullValues() { */ public RedisCacheConfiguration disableKeyPrefix() { - return new RedisCacheConfiguration(ttl, cacheNullValues, false, keyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, false, + keyPrefix, keySerializationPair, valueSerializationPair, conversionService); } /** @@ -209,7 +227,7 @@ public RedisCacheConfiguration withConversionService(ConversionService conversio Assert.notNull(conversionService, "ConversionService must not be null!"); - return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair, conversionService); } @@ -223,7 +241,7 @@ public RedisCacheConfiguration serializeKeysWith(SerializationPair keySe Assert.notNull(keySerializationPair, "KeySerializationPair must not be null!"); - return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair, conversionService); } @@ -237,8 +255,8 @@ public RedisCacheConfiguration serializeValuesWith(SerializationPair valueSer Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null!"); - return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair, - valueSerializationPair, conversionService); + return new RedisCacheConfiguration(ttl, maxIdle, cacheNullValues, usePrefix, keyPrefix, + keySerializationPair, valueSerializationPair, conversionService); } /** @@ -297,6 +315,13 @@ public ConversionService getConversionService() { return conversionService; } + /** + * @return The max idle time for cache entries. Never {@literal null}. + */ + public Duration getMaxIdle() { + return maxIdle; + } + /** * Add a {@link Converter} for extracting the {@link String} representation of a cache key if no suitable * {@link Object#toString()} method is present. 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 19e62f8bd6..1ac6f0113e 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -32,6 +32,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Shyngys Sapraliyev * @since 2.0 */ public interface RedisCacheWriter extends CacheStatisticsProvider { @@ -96,18 +97,20 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio * @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 maxIdle Optional max idle time. Can be {@literal null}. */ - void put(String name, byte[] key, byte[] value, @Nullable Duration ttl); + void put(String name, byte[] key, byte[] value, @Nullable Duration ttl, @Nullable Duration maxIdle); /** * Get the binary value representation from Redis stored for the given key. * * @param name must not be {@literal null}. * @param key must not be {@literal null}. + * @param maxIdle Optional max idle time. Can be {@literal null}. * @return {@literal null} if key does not exist. */ @Nullable - byte[] get(String name, byte[] key); + byte[] get(String name, byte[] key, @Nullable Duration maxIdle); /** * Write the given value to Redis if the key does not already exist. @@ -116,10 +119,11 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio * @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 maxIdle Optional max idle time. Can be {@literal null}. * @return {@literal null} if the value has been written, the value stored for the key if it already exists. */ @Nullable - byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl); + byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl, @Nullable Duration maxIdle); /** * Remove the given key from Redis. diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java index 21121b20fe..c0b252c1c7 100644 --- a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java +++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java @@ -81,7 +81,7 @@ void putShouldAddEternalEntry() { RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) .withStatisticsCollector(CacheStatisticsCollector.create()); - writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO); + writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO, Duration.ZERO); doWithConnection(connection -> { assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue); @@ -96,7 +96,7 @@ void putShouldAddEternalEntry() { void putShouldAddExpiringEntry() { nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, - Duration.ofSeconds(1)); + Duration.ofSeconds(1), Duration.ZERO); doWithConnection(connection -> { assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue); @@ -109,7 +109,8 @@ void putShouldOverwriteExistingEternalEntry() { doWithConnection(connection -> connection.set(binaryCacheKey, "foo".getBytes())); - nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO); + nonLockingRedisCacheWriter(connectionFactory) + .put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO, Duration.ZERO); doWithConnection(connection -> { assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue); @@ -124,7 +125,7 @@ void putShouldOverwriteExistingExpiringEntryAndResetTtl() { Expiration.from(1, TimeUnit.MINUTES), SetOption.upsert())); nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, - Duration.ofSeconds(5)); + Duration.ofSeconds(5), Duration.ZERO); doWithConnection(connection -> { assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue); @@ -139,7 +140,7 @@ void getShouldReturnValue() { RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) .withStatisticsCollector(CacheStatisticsCollector.create()); - assertThat(writer.get(CACHE_NAME, binaryCacheKey)) + assertThat(writer.get(CACHE_NAME, binaryCacheKey, Duration.ZERO)) .isEqualTo(binaryCacheValue); assertThat(writer.getCacheStatistics(CACHE_NAME).getGets()).isOne(); @@ -149,7 +150,8 @@ void getShouldReturnValue() { @ParameterizedRedisTest // DATAREDIS-481 void getShouldReturnNullWhenKeyDoesNotExist() { - assertThat(nonLockingRedisCacheWriter(connectionFactory).get(CACHE_NAME, binaryCacheKey)).isNull(); + assertThat(nonLockingRedisCacheWriter(connectionFactory) + .get(CACHE_NAME, binaryCacheKey, Duration.ZERO)).isNull(); } @ParameterizedRedisTest // DATAREDIS-481, DATAREDIS-1082 @@ -158,7 +160,7 @@ void putIfAbsentShouldAddEternalEntryWhenKeyDoesNotExist() { RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) .withStatisticsCollector(CacheStatisticsCollector.create()); assertThat(writer.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, - Duration.ZERO)).isNull(); + Duration.ZERO, Duration.ZERO)).isNull(); doWithConnection(connection -> { assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue); @@ -174,7 +176,7 @@ void putIfAbsentShouldNotAddEternalEntryWhenKeyAlreadyExist() { RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) .withStatisticsCollector(CacheStatisticsCollector.create()); - assertThat(writer.putIfAbsent(CACHE_NAME, binaryCacheKey, "foo".getBytes(), Duration.ZERO)) + assertThat(writer.putIfAbsent(CACHE_NAME, binaryCacheKey, "foo".getBytes(), Duration.ZERO, Duration.ZERO)) .isEqualTo(binaryCacheValue); doWithConnection(connection -> { @@ -189,7 +191,8 @@ void putIfAbsentShouldAddExpiringEntryWhenKeyDoesNotExist() { RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory) .withStatisticsCollector(CacheStatisticsCollector.create()); - assertThat(writer.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ofSeconds(5))).isNull(); + assertThat(writer.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ofSeconds(5), Duration.ZERO)) + .isNull(); doWithConnection(connection -> { assertThat(connection.ttl(binaryCacheKey)).isGreaterThan(3).isLessThan(6); @@ -234,7 +237,8 @@ void nonLockingCacheWriterShouldIgnoreExistingLock() { ((DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory)).lock(CACHE_NAME); - nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO); + nonLockingRedisCacheWriter(connectionFactory) + .put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO, Duration.ZERO); doWithConnection(connection -> { assertThat(connection.exists(binaryCacheKey)).isTrue(); @@ -246,8 +250,8 @@ void lockingCacheWriterShouldIgnoreExistingLockOnDifferenceCache() { ((DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory)).lock(CACHE_NAME); - lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME + "-no-the-other-cache", binaryCacheKey, binaryCacheValue, - Duration.ZERO); + lockingRedisCacheWriter(connectionFactory) + .put(CACHE_NAME + "-no-the-other-cache", binaryCacheKey, binaryCacheValue, Duration.ZERO, Duration.ZERO); doWithConnection(connection -> { assertThat(connection.exists(binaryCacheKey)).isTrue(); @@ -267,7 +271,7 @@ void lockingCacheWriterShouldWaitForLockRelease() throws InterruptedException { Thread th = new Thread(() -> { beforeWrite.countDown(); - writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO); + writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO, Duration.ZERO); afterWrite.countDown(); }); th.start(); @@ -317,7 +321,7 @@ boolean doCheckLock(String name, RedisConnection connection) { }; try { - writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO); + writer.put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO, Duration.ZERO); } catch (Exception e) { exceptionRef.set(e); } finally { @@ -342,7 +346,7 @@ void noOpSatisticsCollectorReturnsEmptyStatsInstance() { DefaultRedisCacheWriter cw = (DefaultRedisCacheWriter) lockingRedisCacheWriter(connectionFactory); CacheStatistics stats = cw.getCacheStatistics(CACHE_NAME); - cw.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ofSeconds(5)); + cw.putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ofSeconds(5), Duration.ZERO); assertThat(stats).isNotNull(); assertThat(stats.getPuts()).isZero(); 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 c836bd01f8..c1e760d04c 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -433,12 +433,13 @@ void multipleThreadsLoadValueOnce() throws InterruptedException { cache = new RedisCache("foo", new RedisCacheWriter() { @Override - public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + public void put(String name, byte[] key, byte[] value, + @Nullable Duration ttl, @Nullable Duration maxIdle) { storage.set(value); } @Override - public byte[] get(String name, byte[] key) { + public byte[] get(String name, byte[] key, @Nullable Duration maxIdle) { prepare.countDown(); try { @@ -451,7 +452,8 @@ public byte[] get(String name, byte[] key) { } @Override - public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) { + public byte[] putIfAbsent(String name, byte[] key, byte[] value, + @Nullable Duration ttl, @Nullable Duration maxIdle) { return new byte[0]; }