Skip to content

Commit 924db01

Browse files
committed
Add support for TTI expiration in Redis Cache implementation.
We now support time-to-idle (TTI) expiration policies for cache reads. The TTI implementation is achieved with the use of the Redis GETEX command on Cache.get(key) operations as well as consistently using the same TTL configuration for all cache operations when TTI is enabled and TTL expiration has been configured, with the use of a TtlFunction or fixed Duration. Closes spring-projects#2351 Original pull request: spring-projects#2643
1 parent 9655db8 commit 924db01

File tree

5 files changed

+111
-36
lines changed

5 files changed

+111
-36
lines changed

src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,14 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
124124
}
125125

126126
@Override
127-
public byte[] get(String name, byte[] key) {
127+
public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
128128

129129
Assert.notNull(name, "Name must not be null");
130130
Assert.notNull(key, "Key must not be null");
131131

132-
byte[] result = execute(name, connection -> connection.get(key));
132+
byte[] result = shouldExpireWithin(ttl)
133+
? execute(name, connection -> connection.getEx(key, Expiration.from(ttl)))
134+
: execute(name, connection -> connection.get(key));
133135

134136
statistics.incGets(name);
135137

src/main/java/org/springframework/data/redis/cache/RedisCache.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.lang.reflect.Method;
1919
import java.nio.ByteBuffer;
20+
import java.time.Duration;
2021
import java.util.Arrays;
2122
import java.util.Collection;
2223
import java.util.Map;
@@ -188,11 +189,21 @@ protected <T> T loadCacheValue(Object key, Callable<T> valueLoader) {
188189
@Override
189190
protected Object lookup(Object key) {
190191

191-
byte[] value = getCacheWriter().get(getName(), createAndConvertCacheKey(key));
192+
byte[] value = getCacheConfiguration().isTtiExpirationEnabled()
193+
? getCacheWriter().get(getName(), createAndConvertCacheKey(key), getTimeToLive(key))
194+
: getCacheWriter().get(getName(), createAndConvertCacheKey(key));
192195

193196
return value != null ? deserializeCacheValue(value) : null;
194197
}
195198

199+
private Duration getTimeToLive(Object key) {
200+
return getTimeToLive(key, null);
201+
}
202+
203+
private Duration getTimeToLive(Object key, @Nullable Object value) {
204+
return getCacheConfiguration().getTtlFunction().getTimeToLive(key, value);
205+
}
206+
196207
@Override
197208
public void put(Object key, @Nullable Object value) {
198209

@@ -208,7 +219,7 @@ public void put(Object key, @Nullable Object value) {
208219
}
209220

210221
getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
211-
getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
222+
getTimeToLive(key, value));
212223
}
213224

214225
@Override
@@ -221,7 +232,7 @@ public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
221232
}
222233

223234
byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key),
224-
serializeCacheValue(cacheValue), getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
235+
serializeCacheValue(cacheValue), getTimeToLive(key, value));
225236

226237
return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null;
227238
}

src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java

+62-17
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@
4747
public class RedisCacheConfiguration {
4848

4949
protected static final boolean DEFAULT_CACHE_NULL_VALUES = true;
50+
protected static final boolean DEFAULT_ENABLE_TTI_EXPIRATION = false;
5051
protected static final boolean DEFAULT_USE_PREFIX = true;
5152
protected static final boolean DO_NOT_CACHE_NULL_VALUES = false;
5253
protected static final boolean DO_NOT_USE_PREFIX = false;
54+
protected static final boolean ENABLE_IDLE_TIME_EXPIRATION = true;
5355

5456
/**
5557
* Default {@link RedisCacheConfiguration} using the following:
@@ -108,14 +110,18 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c
108110

109111
registerDefaultConverters(conversionService);
110112

111-
return new RedisCacheConfiguration(TtlFunction.persistent(), DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX,
113+
return new RedisCacheConfiguration(TtlFunction.persistent(),
114+
DEFAULT_CACHE_NULL_VALUES,
115+
DEFAULT_ENABLE_TTI_EXPIRATION,
116+
DEFAULT_USE_PREFIX,
112117
CacheKeyPrefix.simple(),
113118
SerializationPair.fromSerializer(RedisSerializer.string()),
114119
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)),
115120
conversionService);
116121
}
117122

118123
private final boolean cacheNullValues;
124+
private final boolean enableTtiExpiration;
119125
private final boolean usePrefix;
120126

121127
private final CacheKeyPrefix keyPrefix;
@@ -128,12 +134,13 @@ public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader c
128134
private final TtlFunction ttlFunction;
129135

130136
@SuppressWarnings("unchecked")
131-
private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean usePrefix,
132-
CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair,
137+
private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean enableTtiExpiration,
138+
Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair,
133139
SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
134140

135141
this.ttlFunction = ttlFunction;
136142
this.cacheNullValues = cacheNullValues;
143+
this.enableTtiExpiration = enableTtiExpiration;
137144
this.usePrefix = usePrefix;
138145
this.keyPrefix = keyPrefix;
139146
this.keySerializationPair = keySerializationPair;
@@ -168,8 +175,9 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix)
168175

169176
Assert.notNull(cacheKeyPrefix, "Function used to compute prefix must not be null");
170177

171-
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DEFAULT_USE_PREFIX,
172-
cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(), getConversionService());
178+
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(),
179+
DEFAULT_USE_PREFIX, cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(),
180+
getConversionService());
173181
}
174182

175183
/**
@@ -181,8 +189,9 @@ public RedisCacheConfiguration computePrefixWith(CacheKeyPrefix cacheKeyPrefix)
181189
* @return new {@link RedisCacheConfiguration}.
182190
*/
183191
public RedisCacheConfiguration disableCachingNullValues() {
184-
return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, usePrefix(), getKeyPrefix(),
185-
getKeySerializationPair(), getValueSerializationPair(), getConversionService());
192+
return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, isTtiExpirationEnabled(),
193+
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(),
194+
getConversionService());
186195
}
187196

188197
/**
@@ -193,8 +202,30 @@ public RedisCacheConfiguration disableCachingNullValues() {
193202
* @return new {@link RedisCacheConfiguration}.
194203
*/
195204
public RedisCacheConfiguration disableKeyPrefix() {
196-
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DO_NOT_USE_PREFIX,
197-
getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService());
205+
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(),
206+
DO_NOT_USE_PREFIX, getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService());
207+
}
208+
209+
/**
210+
* Enables {@literal time-to-idle (TTI) expiration} on {@link Cache} read operations,
211+
* such as {@link Cache#get(Object)}.
212+
* <p>
213+
* Enabling this option applies the same {@link #getTtlFunction() TTL expiration policy} to {@link Cache} read
214+
* operations as it does for {@link Cache} write operations. In effect, this will invoke the Redis {@literal GETEX}
215+
* command in place of {@literal GET}.
216+
* <p>
217+
* Redis does not support the concept of {@literal TTI}, only {@literal TTL}. However, if {@literal TTL} expiration
218+
* is applied to all {@link Cache} operations, both read and write alike, and {@link Cache} operations passed with
219+
* expiration are used consistently across the application, then in effect, an application can achieve
220+
* {@literal TTI} expiration-like behavior.
221+
*
222+
* @return this {@link RedisCacheConfiguration}.
223+
* @see <a href="https://redis.io/commands/getex/">GETEX</a>
224+
*/
225+
public RedisCacheConfiguration enableTtiExpiration() {
226+
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), ENABLE_IDLE_TIME_EXPIRATION,
227+
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(),
228+
getConversionService());
198229
}
199230

200231
/**
@@ -222,8 +253,9 @@ public RedisCacheConfiguration entryTtl(TtlFunction ttlFunction) {
222253

223254
Assert.notNull(ttlFunction, "TtlFunction must not be null");
224255

225-
return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
226-
getKeySerializationPair(), getValueSerializationPair(), getConversionService());
256+
return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), isTtiExpirationEnabled(),
257+
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(),
258+
getConversionService());
227259
}
228260

229261
/**
@@ -236,8 +268,8 @@ public RedisCacheConfiguration serializeKeysWith(SerializationPair<String> keySe
236268

237269
Assert.notNull(keySerializationPair, "KeySerializationPair must not be null");
238270

239-
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
240-
keySerializationPair, getValueSerializationPair(), getConversionService());
271+
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(),
272+
usePrefix(), getKeyPrefix(), keySerializationPair, getValueSerializationPair(), getConversionService());
241273
}
242274

243275
/**
@@ -250,8 +282,8 @@ public RedisCacheConfiguration serializeValuesWith(SerializationPair<?> valueSer
250282

251283
Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null");
252284

253-
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
254-
getKeySerializationPair(), valueSerializationPair, getConversionService());
285+
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(),
286+
usePrefix(), getKeyPrefix(), getKeySerializationPair(), valueSerializationPair, getConversionService());
255287
}
256288

257289
/**
@@ -264,8 +296,8 @@ public RedisCacheConfiguration withConversionService(ConversionService conversio
264296

265297
Assert.notNull(conversionService, "ConversionService must not be null");
266298

267-
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
268-
getKeySerializationPair(), getValueSerializationPair(), conversionService);
299+
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTtiExpirationEnabled(),
300+
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), conversionService);
269301
}
270302

271303
/**
@@ -275,6 +307,19 @@ public boolean getAllowCacheNullValues() {
275307
return this.cacheNullValues;
276308
}
277309

310+
/**
311+
* Determines whether {@literal time-to-idle (TTI) expiration} has been enabled for caching.
312+
* <p>
313+
* Use {@link #enableTtiExpiration()} to opt-in and enable {@literal time-to-idle (TTI) expiration} for caching.
314+
*
315+
* @return {@literal true} if {@literal time-to-idle (TTI) expiration} was configured and enabled for caching.
316+
* Defaults to {@literal false}.
317+
* @see <a href="https://redis.io/commands/getex/">GETEX</a>
318+
*/
319+
public boolean isTtiExpirationEnabled() {
320+
return this.enableTtiExpiration;
321+
}
322+
278323
/**
279324
* @return {@literal true} if cache keys need to be prefixed with the {@link #getKeyPrefixFor(String)} if present or
280325
* the default which resolves to {@link Cache#getName()}.

src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java

+29-13
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio
8585
*/
8686
static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory,
8787
BatchStrategy batchStrategy) {
88-
return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(), batchStrategy);
88+
89+
return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(),
90+
batchStrategy);
8991
}
9092

9193
/**
@@ -104,29 +106,43 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio
104106

105107
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
106108

107-
return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction, CacheStatisticsCollector.none(),
108-
batchStrategy);
109+
return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction,
110+
CacheStatisticsCollector.none(), batchStrategy);
109111
}
110112

111113
/**
112-
* Write the given key/value pair to Redis and set the expiration time if defined.
114+
* Get the binary value representation from Redis stored for the given key.
113115
*
114-
* @param name The cache name must not be {@literal null}.
115-
* @param key The key for the cache entry. Must not be {@literal null}.
116-
* @param value The value stored for the key. Must not be {@literal null}.
117-
* @param ttl Optional expiration time. Can be {@literal null}.
116+
* @param name must not be {@literal null}.
117+
* @param key must not be {@literal null}.
118+
* @return {@literal null} if key does not exist.
119+
* @see #get(String, byte[], Duration)
118120
*/
119-
void put(String name, byte[] key, byte[] value, @Nullable Duration ttl);
121+
@Nullable
122+
default byte[] get(String name, byte[] key) {
123+
return get(name, key, null);
124+
}
120125

121126
/**
122-
* Get the binary value representation from Redis stored for the given key.
127+
* Get the binary value representation from Redis stored for the given key and set the given
128+
* {@link Duration TTL expiration} for the cache entry.
123129
*
124130
* @param name must not be {@literal null}.
125131
* @param key must not be {@literal null}.
126-
* @return {@literal null} if key does not exist.
132+
* @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry.
133+
* @return {@literal null} if key does not exist or has {@literal expired}.
127134
*/
128-
@Nullable
129-
byte[] get(String name, byte[] key);
135+
byte[] get(String name, byte[] key, @Nullable Duration ttl);
136+
137+
/**
138+
* Write the given key/value pair to Redis and set the expiration time if defined.
139+
*
140+
* @param name The cache name must not be {@literal null}.
141+
* @param key The key for the cache entry. Must not be {@literal null}.
142+
* @param value The value stored for the key. Must not be {@literal null}.
143+
* @param ttl Optional expiration time. Can be {@literal null}.
144+
*/
145+
void put(String name, byte[] key, byte[] value, @Nullable Duration ttl);
130146

131147
/**
132148
* Write the given value to Redis if the key does not already exist.

src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -455,13 +455,14 @@ void multipleThreadsLoadValueOnce() throws InterruptedException {
455455
AtomicReference<byte[]> storage = new AtomicReference<>();
456456

457457
cache = new RedisCache("foo", new RedisCacheWriter() {
458+
458459
@Override
459460
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
460461
storage.set(value);
461462
}
462463

463464
@Override
464-
public byte[] get(String name, byte[] key) {
465+
public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
465466

466467
prepare.countDown();
467468
try {

0 commit comments

Comments
 (0)