Skip to content

Commit 770fbac

Browse files
committed
Review and polish for confiuring a cache lock TTL.
Closes #2300 Pull request: #2597.
1 parent 9aa2eb4 commit 770fbac

File tree

8 files changed

+276
-217
lines changed

8 files changed

+276
-217
lines changed

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

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,34 @@
3131

3232
/**
3333
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
34-
* and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual
35-
* {@link RedisConnection}. <br />
34+
* and {@literal cluster} environments, and uses a given {@link RedisConnectionFactory} to obtain the actual
35+
* {@link RedisConnection}.
36+
* <p>
3637
* {@link DefaultRedisCacheWriter} can be used in
37-
* {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or
38-
* {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While
39-
* {@literal non-locking} aims for maximum performance it may result in overlapping, non atomic, command execution for
38+
* {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking}
39+
* or {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While
40+
* {@literal non-locking} aims for maximum performance it may result in overlapping, non-atomic, command execution for
4041
* operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents
4142
* command overlap by setting an explicit lock key and checking against presence of this key which leads to additional
4243
* requests and potential command wait times.
4344
*
4445
* @author Christoph Strobl
4546
* @author Mark Paluch
4647
* @author André Prata
48+
* @author John Blum
4749
* @since 2.0
4850
*/
4951
class DefaultRedisCacheWriter implements RedisCacheWriter {
5052

51-
private final RedisConnectionFactory connectionFactory;
53+
private final BatchStrategy batchStrategy;
54+
55+
private final CacheStatisticsCollector statistics;
56+
5257
private final Duration sleepTime;
5358

59+
private final RedisConnectionFactory connectionFactory;
60+
5461
private final TtlFunction lockTtl;
55-
private final CacheStatisticsCollector statistics;
56-
private final BatchStrategy batchStrategy;
5762

5863
/**
5964
* @param connectionFactory must not be {@literal null}.
@@ -166,8 +171,8 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat
166171
}
167172

168173
return connection.get(key);
169-
} finally {
170174

175+
} finally {
171176
if (isLockingCacheWriter()) {
172177
doUnlock(name, connection);
173178
}
@@ -196,21 +201,21 @@ public void clean(String name, byte[] pattern) {
196201
boolean wasLocked = false;
197202

198203
try {
199-
200204
if (isLockingCacheWriter()) {
201205
doLock(name, name, pattern, connection);
202206
wasLocked = true;
203207
}
204208

205209
long deleteCount = batchStrategy.cleanCache(connection, name, pattern);
210+
206211
while (deleteCount > Integer.MAX_VALUE) {
207212
statistics.incDeletesBy(name, Integer.MAX_VALUE);
208213
deleteCount -= Integer.MAX_VALUE;
209214
}
215+
210216
statistics.incDeletesBy(name, (int) deleteCount);
211217

212218
} finally {
213-
214219
if (wasLocked && isLockingCacheWriter()) {
215220
doUnlock(name, connection);
216221
}
@@ -280,8 +285,8 @@ private boolean isLockingCacheWriter() {
280285
private <T> T execute(String name, Function<RedisConnection, T> callback) {
281286

282287
RedisConnection connection = connectionFactory.getConnection();
283-
try {
284288

289+
try {
285290
checkAndPotentiallyWaitUntilUnlocked(name, connection);
286291
return callback.apply(connection);
287292
} finally {
@@ -307,18 +312,19 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c
307312
}
308313

309314
long lockWaitTimeNs = System.nanoTime();
310-
try {
311315

316+
try {
312317
while (doCheckLock(name, connection)) {
313318
Thread.sleep(sleepTime.toMillis());
314319
}
315-
} catch (InterruptedException ex) {
320+
} catch (InterruptedException cause) {
316321

317322
// Re-interrupt current thread, to allow other participants to react.
318323
Thread.currentThread().interrupt();
319324

320-
throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
321-
ex);
325+
String message = String.format("Interrupted while waiting to unlock cache %s", name);
326+
327+
throw new PessimisticLockingFailureException(message, cause);
322328
} finally {
323329
statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs);
324330
}

src/main/java/org/springframework/data/redis/cache/SingletonTtlFunction.java renamed to src/main/java/org/springframework/data/redis/cache/FixedDurationTtlFunction.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@
2121
import org.springframework.lang.Nullable;
2222

2323
/**
24-
* Singleton implementation of {@link TtlFunction}.
24+
* {@link TtlFunction} implementation returning the given, predetermined {@link Duration} used for per cache entry
25+
* {@literal time-to-live (TTL) expiration}.
2526
*
2627
* @author Mark Paluch
28+
* @author John Blum
29+
* @see java.time.Duration
30+
* @see org.springframework.data.redis.cache.RedisCacheWriter.TtlFunction
2731
* @since 3.2
2832
*/
29-
public record SingletonTtlFunction(Duration duration) implements TtlFunction {
33+
public record FixedDurationTtlFunction(Duration duration) implements TtlFunction {
3034

3135
@Override
3236
public Duration getTimeToLive(Object key, @Nullable Object value) {

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

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ public class RedisCache extends AbstractValueAdaptingCache {
6767
* Create a new {@link RedisCache}.
6868
*
6969
* @param name {@link String name} for this {@link Cache}; must not be {@literal null}.
70-
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing appropriate
71-
* Redis commands; must not be {@literal null}.
72-
* @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache on creation; must not
73-
* be {@literal null}.
70+
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing
71+
* the necessary Redis commands; must not be {@literal null}.
72+
* @param cacheConfiguration {@link RedisCacheConfiguration} applied to this {@link RedisCache} on creation;
73+
* must not be {@literal null}.
7474
* @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration}
7575
* are {@literal null} or the given {@link String} name for this {@link RedisCache} is {@literal null}.
7676
*/
@@ -88,18 +88,33 @@ protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfig
8888
}
8989

9090
/**
91-
* Get {@link RedisCacheConfiguration} used.
91+
* Get the {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization.
9292
*
93-
* @return immutable {@link RedisCacheConfiguration}. Never {@literal null}.
93+
* @return an immutable {@link RedisCacheConfiguration} used to configure this {@link RedisCache} on initialization;
94+
* never {@literal null}.
9495
*/
9596
public RedisCacheConfiguration getCacheConfiguration() {
9697
return this.cacheConfiguration;
9798
}
9899

100+
/**
101+
* Gets the configured {@link RedisCacheWriter} used to modify Redis for cache operations.
102+
*
103+
* @return the configured {@link RedisCacheWriter} used to modify Redis for cache operations.
104+
*/
99105
protected RedisCacheWriter getCacheWriter() {
100106
return this.cacheWriter;
101107
}
102108

109+
/**
110+
* Gets the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String}
111+
* when accessing entries in the cache.
112+
*
113+
* @return the configured {@link ConversionService} used to convert {@link Object cache keys} to a {@link String}
114+
* when accessing entries in the cache.
115+
* @see RedisCacheConfiguration#getConversionService()
116+
* @see #getCacheConfiguration()
117+
*/
103118
protected ConversionService getConversionService() {
104119
return getCacheConfiguration().getConversionService();
105120
}
@@ -111,12 +126,13 @@ public String getName() {
111126

112127
@Override
113128
public RedisCacheWriter getNativeCache() {
114-
return this.cacheWriter;
129+
return getCacheWriter();
115130
}
116131

117132
/**
118-
* Return the {@link CacheStatistics} snapshot for this cache instance. Statistics are accumulated per cache instance
119-
* and not from the backing Redis data store.
133+
* Return the {@link CacheStatistics} snapshot for this cache instance.
134+
* <p>
135+
* Statistics are accumulated per cache instance and not from the backing Redis data store.
120136
*
121137
* @return statistics object for this {@link RedisCache}.
122138
* @since 2.4
@@ -134,15 +150,25 @@ public <T> T get(Object key, Callable<T> valueLoader) {
134150
return result != null ? (T) result.get() : getSynchronized(key, valueLoader);
135151
}
136152

137-
@SuppressWarnings("unchecked")
138153
@Nullable
154+
@SuppressWarnings("unchecked")
139155
private synchronized <T> T getSynchronized(Object key, Callable<T> valueLoader) {
140156

141157
ValueWrapper result = get(key);
142158

143159
return result != null ? (T) result.get() : loadCacheValue(key, valueLoader);
144160
}
145161

162+
/**
163+
* Loads the {@link Object} using the given {@link Callable valueLoader} and {@link #put(Object, Object) puts}
164+
* the {@link Object loaded value} in the cache.
165+
*
166+
* @param <T> {@link Class type} of the loaded {@link Object cache value}.
167+
* @param key {@link Object key} mapped to the loaded {@link Object cache value}.
168+
* @param valueLoader {@link Callable} object used to load the {@link Object value}
169+
* for the given {@link Object key}.
170+
* @return the loaded {@link Object value}.
171+
*/
146172
protected <T> T loadCacheValue(Object key, Callable<T> valueLoader) {
147173

148174
T value;
@@ -172,7 +198,7 @@ public void put(Object key, @Nullable Object value) {
172198

173199
Object cacheValue = preProcessCacheValue(value);
174200

175-
if (!isAllowNullValues() && cacheValue == null) {
201+
if (nullCacheValueIsNotAllowed(cacheValue)) {
176202

177203
String message = String.format("Cache '%s' does not allow 'null' values; Avoid storing null"
178204
+ " via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null'"
@@ -182,20 +208,20 @@ public void put(Object key, @Nullable Object value) {
182208
}
183209

184210
getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
185-
getCacheConfiguration().getTtl());
211+
getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
186212
}
187213

188214
@Override
189215
public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
190216

191217
Object cacheValue = preProcessCacheValue(value);
192218

193-
if (!isAllowNullValues() && cacheValue == null) {
219+
if (nullCacheValueIsNotAllowed(cacheValue)) {
194220
return get(key);
195221
}
196222

197223
byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key),
198-
serializeCacheValue(cacheValue), getCacheConfiguration().getTtl());
224+
serializeCacheValue(cacheValue), getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
199225

200226
return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null;
201227
}
@@ -206,7 +232,7 @@ public void clear() {
206232
}
207233

208234
/**
209-
* Clear keys that match the provided {@link String keyPattern}.
235+
* Clear keys that match the given {@link String keyPattern}.
210236
* <p>
211237
* Useful when cache keys are formatted in a style where Redis patterns can be used for matching these.
212238
*
@@ -303,7 +329,7 @@ protected String createCacheKey(Object key) {
303329
}
304330

305331
/**
306-
* Convert {@code key} to a {@link String} representation used for cache key creation.
332+
* Convert {@code key} to a {@link String} used in cache key creation.
307333
*
308334
* @param key will never be {@literal null}.
309335
* @return never {@literal null}.
@@ -338,10 +364,9 @@ protected String convertKey(Object key) {
338364
return key.toString();
339365
}
340366

341-
String message = String.format(
342-
"Cannot convert cache key %s to String; Please register a suitable Converter"
367+
String message = String.format("Cannot convert cache key %s to String; Please register a suitable Converter"
343368
+ " via 'RedisCacheConfiguration.configureKeyConverters(...)' or override '%s.toString()'",
344-
source, key.getClass().getName());
369+
source, key.getClass().getName());
345370

346371
throw new IllegalStateException(message);
347372
}
@@ -403,4 +428,8 @@ private String prefixCacheKey(String key) {
403428
// allow contextual cache names by computing the key prefix on every call.
404429
return getCacheConfiguration().getKeyPrefixFor(getName()) + key;
405430
}
431+
432+
private boolean nullCacheValueIsNotAllowed(@Nullable Object cacheValue) {
433+
return cacheValue == null && !isAllowNullValues();
434+
}
406435
}

0 commit comments

Comments
 (0)