Skip to content

Commit 06bbd3d

Browse files
mp911dechristophstrobl
authored andcommitted
Use Redis locking for value retrieval synchronization.
We now use RedisCacheWriter to acquire and maintain the lock for value retrieval synchronization. Closes: #2890 Original Pull Request: #2948
1 parent 777f079 commit 06bbd3d

File tree

5 files changed

+169
-160
lines changed

5 files changed

+169
-160
lines changed

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

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.concurrent.TimeUnit;
2626
import java.util.concurrent.atomic.AtomicLong;
2727
import java.util.function.Function;
28+
import java.util.function.Supplier;
2829

2930
import org.springframework.dao.PessimisticLockingFailureException;
3031
import org.springframework.data.redis.connection.ReactiveRedisConnection;
@@ -137,9 +138,14 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
137138
Assert.notNull(name, "Name must not be null");
138139
Assert.notNull(key, "Key must not be null");
139140

140-
byte[] result = shouldExpireWithin(ttl)
141-
? execute(name, connection -> connection.stringCommands().getEx(key, Expiration.from(ttl)))
142-
: execute(name, connection -> connection.stringCommands().get(key));
141+
return execute(name, connection -> doGet(connection, name, key, ttl));
142+
}
143+
144+
@Nullable
145+
private byte[] doGet(RedisConnection connection, String name, byte[] key, @Nullable Duration ttl) {
146+
147+
byte[] result = shouldExpireWithin(ttl) ? connection.stringCommands().getEx(key, Expiration.from(ttl))
148+
: connection.stringCommands().get(key);
143149

144150
statistics.incGets(name);
145151

@@ -152,6 +158,50 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
152158
return result;
153159
}
154160

161+
@Override
162+
public byte[] get(String name, byte[] key, Supplier<byte[]> valueLoader, @Nullable Duration ttl,
163+
boolean timeToIdleEnabled) {
164+
165+
Assert.notNull(name, "Name must not be null");
166+
Assert.notNull(key, "Key must not be null");
167+
168+
boolean withTtl = shouldExpireWithin(ttl);
169+
170+
// double-checked locking optimization
171+
if (isLockingCacheWriter()) {
172+
byte[] bytes = get(name, key, timeToIdleEnabled && withTtl ? ttl : null);
173+
if (bytes != null) {
174+
return bytes;
175+
}
176+
}
177+
178+
return execute(name, connection -> {
179+
180+
boolean wasLocked = false;
181+
if (isLockingCacheWriter()) {
182+
doLock(name, key, null, connection);
183+
wasLocked = true;
184+
}
185+
186+
try {
187+
188+
byte[] result = doGet(connection, name, key, timeToIdleEnabled && withTtl ? ttl : null);
189+
190+
if (result != null) {
191+
return result;
192+
}
193+
194+
byte[] value = valueLoader.get();
195+
doPut(connection, name, key, value, ttl);
196+
return value;
197+
} finally {
198+
if (isLockingCacheWriter() && wasLocked) {
199+
doUnlock(name, connection);
200+
}
201+
}
202+
});
203+
}
204+
155205
@Override
156206
public boolean supportsAsyncRetrieve() {
157207
return asyncCacheWriter.isSupported();
@@ -186,17 +236,21 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
186236
Assert.notNull(value, "Value must not be null");
187237

188238
execute(name, connection -> {
189-
190-
if (shouldExpireWithin(ttl)) {
191-
connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS),
192-
SetOption.upsert());
193-
} else {
194-
connection.stringCommands().set(key, value);
195-
}
196-
239+
doPut(connection, name, key, value, ttl);
197240
return "OK";
198241
});
199242

243+
}
244+
245+
private void doPut(RedisConnection connection, String name, byte[] key, byte[] value, @Nullable Duration ttl) {
246+
247+
if (shouldExpireWithin(ttl)) {
248+
connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS),
249+
SetOption.upsert());
250+
} else {
251+
connection.stringCommands().set(key, value);
252+
}
253+
200254
statistics.incPuts(name);
201255
}
202256

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

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
import java.util.StringJoiner;
2626
import java.util.concurrent.Callable;
2727
import java.util.concurrent.CompletableFuture;
28-
import java.util.concurrent.locks.Lock;
29-
import java.util.concurrent.locks.ReentrantLock;
3028
import java.util.function.Supplier;
3129

3230
import org.springframework.cache.Cache;
@@ -64,8 +62,6 @@ public class RedisCache extends AbstractValueAdaptingCache {
6462

6563
static final String CACHE_RETRIEVAL_UNSUPPORTED_OPERATION_EXCEPTION_MESSAGE = "The Redis driver configured with RedisCache through RedisCacheWriter does not support CompletableFuture-based retrieval";
6664

67-
private final Lock lock = new ReentrantLock();
68-
6965
private final RedisCacheConfiguration cacheConfiguration;
7066

7167
private final RedisCacheWriter cacheWriter;
@@ -154,28 +150,18 @@ public CacheStatistics getStatistics() {
154150
@SuppressWarnings("unchecked")
155151
public <T> T get(Object key, Callable<T> valueLoader) {
156152

157-
ValueWrapper result = get(key);
158-
159-
return result != null ? (T) result.get() : getSynchronized(key, valueLoader);
160-
}
161-
162-
@Nullable
163-
@SuppressWarnings("unchecked")
164-
private <T> T getSynchronized(Object key, Callable<T> valueLoader) {
153+
byte[] binaryKey = createAndConvertCacheKey(key);
154+
byte[] binaryValue = getCacheWriter().get(getName(), binaryKey,
155+
() -> serializeCacheValue(toStoreValue(loadCacheValue(key, valueLoader))), getTimeToLive(key),
156+
getCacheConfiguration().isTimeToIdleEnabled());
165157

166-
lock.lock();
158+
ValueWrapper result = toValueWrapper(deserializeCacheValue(binaryValue));
167159

168-
try {
169-
ValueWrapper result = get(key);
170-
return result != null ? (T) result.get() : loadCacheValue(key, valueLoader);
171-
} finally {
172-
lock.unlock();
173-
}
160+
return result != null ? (T) result.get() : null;
174161
}
175162

176163
/**
177-
* Loads the {@link Object} using the given {@link Callable valueLoader} and {@link #put(Object, Object) puts} the
178-
* {@link Object loaded value} in the cache.
164+
* Loads the {@link Object} using the given {@link Callable valueLoader}.
179165
*
180166
* @param <T> {@link Class type} of the loaded {@link Object cache value}.
181167
* @param key {@link Object key} mapped to the loaded {@link Object cache value}.
@@ -184,17 +170,11 @@ private <T> T getSynchronized(Object key, Callable<T> valueLoader) {
184170
*/
185171
protected <T> T loadCacheValue(Object key, Callable<T> valueLoader) {
186172

187-
T value;
188-
189173
try {
190-
value = valueLoader.call();
174+
return valueLoader.call();
191175
} catch (Exception ex) {
192176
throw new ValueRetrievalException(key, valueLoader, ex);
193177
}
194-
195-
put(key, value);
196-
197-
return value;
198178
}
199179

200180
@Override

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

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
import org.springframework.util.Assert;
2525

2626
/**
27-
* {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...})
28-
* used for caching.
27+
* {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) used for
28+
* caching.
2929
* <p>
3030
* The {@link RedisCacheWriter} may be shared by multiple cache implementations and is responsible for reading/writing
3131
* binary data from/to Redis. The implementation honors potential cache lock flags that might be set.
3232
* <p>
33-
* The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy}
34-
* to tune performance behavior.
33+
* The default {@link RedisCacheWriter} implementation can be customized with {@link BatchStrategy} to tune performance
34+
* behavior.
3535
*
3636
* @author Christoph Strobl
3737
* @author Mark Paluch
@@ -96,9 +96,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio
9696
*
9797
* @param connectionFactory must not be {@literal null}.
9898
* @param sleepTime sleep time between lock access attempts, must not be {@literal null}.
99-
* @param lockTtlFunction TTL function to compute the Lock TTL. The function is called with contextual keys
100-
* and values (such as the cache name on cleanup or the actual key/value on put requests);
101-
* must not be {@literal null}.
99+
* @param lockTtlFunction TTL function to compute the Lock TTL. The function is called with contextual keys and values
100+
* (such as the cache name on cleanup or the actual key/value on put requests); must not be {@literal null}.
102101
* @param batchStrategy must not be {@literal null}.
103102
* @return new instance of {@link DefaultRedisCacheWriter}.
104103
* @since 3.2
@@ -124,8 +123,8 @@ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectio
124123
byte[] get(String name, byte[] key);
125124

126125
/**
127-
* Get the binary value representation from Redis stored for the given key and set
128-
* the given {@link Duration TTL expiration} for the cache entry.
126+
* Get the binary value representation from Redis stored for the given key and set the given {@link Duration TTL
127+
* expiration} for the cache entry.
129128
*
130129
* @param name must not be {@literal null}.
131130
* @param key must not be {@literal null}.
@@ -138,14 +137,41 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) {
138137
}
139138

140139
/**
141-
* Determines whether the asynchronous {@link #retrieve(String, byte[])}
142-
* and {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation.
140+
* Get the binary value representation from Redis stored for the given key and set the given {@link Duration TTL
141+
* expiration} for the cache entry, obtaining the value from {@code valueLoader} if necessary.
143142
* <p>
144-
* The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined
145-
* by the Redis driver in use at runtime.
143+
* If possible (and configured for locking), implementations should ensure that the loading operation is synchronized
144+
* so that the specified {@code valueLoader} is only called once in case of concurrent access on the same key.
145+
*
146+
* @param name must not be {@literal null}.
147+
* @param key must not be {@literal null}.
148+
* @param valueLoader value loader that creates the value if the cache lookup has been not successful.
149+
* @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry.
150+
* @param timeToIdleEnabled {@literal true} to enable Time to Idle when retrieving the value.
151+
* @since 3.4
152+
*/
153+
default byte[] get(String name, byte[] key, Supplier<byte[]> valueLoader, @Nullable Duration ttl,
154+
boolean timeToIdleEnabled) {
155+
156+
byte[] bytes = timeToIdleEnabled ? get(name, key, ttl) : get(name, key);
157+
158+
if (bytes == null) {
159+
bytes = valueLoader.get();
160+
put(name, key, bytes, ttl);
161+
}
162+
163+
return bytes;
164+
}
165+
166+
/**
167+
* Determines whether the asynchronous {@link #retrieve(String, byte[])} and
168+
* {@link #retrieve(String, byte[], Duration)} cache operations are supported by the implementation.
169+
* <p>
170+
* The main factor for whether the {@literal retrieve} operation can be supported will primarily be determined by the
171+
* Redis driver in use at runtime.
146172
* <p>
147-
* Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)}
148-
* and {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}.
173+
* Returns {@literal false} by default. This will have an effect of {@link RedisCache#retrieve(Object)} and
174+
* {@link RedisCache#retrieve(Object, Supplier)} throwing an {@link UnsupportedOperationException}.
149175
*
150176
* @return {@literal true} if asynchronous {@literal retrieve} operations are supported by the implementation.
151177
* @since 3.2
@@ -155,8 +181,8 @@ default boolean supportsAsyncRetrieve() {
155181
}
156182

157183
/**
158-
* Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache}
159-
* maps the given {@link byte[] key}.
184+
* Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps the given
185+
* {@link byte[] key}.
160186
* <p>
161187
* This operation is non-blocking.
162188
*
@@ -171,8 +197,8 @@ default CompletableFuture<byte[]> retrieve(String name, byte[] key) {
171197
}
172198

173199
/**
174-
* Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps
175-
* the given {@link byte[] key} setting the {@link Duration TTL expiration} for the cache entry.
200+
* Asynchronously retrieves the {@link CompletableFuture value} to which the {@link RedisCache} maps the given
201+
* {@link byte[] key} setting the {@link Duration TTL expiration} for the cache entry.
176202
* <p>
177203
* This operation is non-blocking.
178204
*
@@ -264,8 +290,8 @@ interface TtlFunction {
264290
/**
265291
* Creates a {@literal Singleton} {@link TtlFunction} using the given {@link Duration}.
266292
*
267-
* @param duration the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry
268-
* does not expire).
293+
* @param duration the time to live. Can be {@link Duration#ZERO} for persistent values (i.e. cache entry does not
294+
* expire).
269295
* @return a singleton {@link TtlFunction} using {@link Duration}.
270296
*/
271297
static TtlFunction just(Duration duration) {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,22 @@ void putIfAbsentShouldAddExpiringEntryWhenKeyDoesNotExist() {
242242
assertThat(writer.getCacheStatistics(CACHE_NAME).getPuts()).isOne();
243243
}
244244

245+
@ParameterizedRedisTest // GH-2890
246+
void getWithValueLoaderShouldStoreCacheValue() {
247+
248+
RedisCacheWriter writer = nonLockingRedisCacheWriter(connectionFactory)
249+
.withStatisticsCollector(CacheStatisticsCollector.create());
250+
251+
writer.get(CACHE_NAME, binaryCacheKey, () -> binaryCacheValue, Duration.ofSeconds(5), true);
252+
253+
doWithConnection(connection -> {
254+
assertThat(connection.ttl(binaryCacheKey)).isGreaterThan(3).isLessThan(6);
255+
});
256+
257+
assertThat(writer.getCacheStatistics(CACHE_NAME).getMisses()).isOne();
258+
assertThat(writer.getCacheStatistics(CACHE_NAME).getPuts()).isOne();
259+
}
260+
245261
@ParameterizedRedisTest // DATAREDIS-481, DATAREDIS-1082
246262
void removeShouldDeleteEntry() {
247263

0 commit comments

Comments
 (0)