Skip to content

Commit a047d0c

Browse files
committed
Implement retrieve(key) and retrieve(key, :Supplier<CompletableFuture<T>>) Cache operations in RedisCache.
Closes #2650
1 parent 64cd30d commit a047d0c

File tree

7 files changed

+698
-79
lines changed

7 files changed

+698
-79
lines changed

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

+125-21
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,29 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18+
import java.nio.ByteBuffer;
1819
import java.nio.charset.StandardCharsets;
1920
import java.time.Duration;
21+
import java.util.concurrent.CompletableFuture;
2022
import java.util.concurrent.TimeUnit;
21-
import java.util.function.Consumer;
23+
import java.util.function.BiFunction;
2224
import java.util.function.Function;
25+
import java.util.function.Supplier;
2326

2427
import org.springframework.dao.PessimisticLockingFailureException;
28+
import org.springframework.data.redis.connection.ReactiveRedisConnection;
29+
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
2530
import org.springframework.data.redis.connection.RedisConnection;
2631
import org.springframework.data.redis.connection.RedisConnectionFactory;
2732
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
2833
import org.springframework.data.redis.core.types.Expiration;
34+
import org.springframework.data.redis.util.ByteUtils;
2935
import org.springframework.lang.Nullable;
3036
import org.springframework.util.Assert;
3137

38+
import reactor.core.publisher.Flux;
39+
import reactor.core.publisher.Mono;
40+
3241
/**
3342
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
3443
* and {@literal cluster} environments, and uses a given {@link RedisConnectionFactory} to obtain the actual
@@ -114,8 +123,8 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
114123
Assert.notNull(key, "Key must not be null");
115124

116125
byte[] result = shouldExpireWithin(ttl)
117-
? execute(name, connection -> connection.stringCommands().getEx(key, Expiration.from(ttl)))
118-
: execute(name, connection -> connection.stringCommands().get(key));
126+
? execute(name, connection -> connection.stringCommands().getEx(key, Expiration.from(ttl)))
127+
: execute(name, connection -> connection.stringCommands().get(key));
119128

120129
statistics.incGets(name);
121130

@@ -128,6 +137,74 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
128137
return result;
129138
}
130139

140+
@Override
141+
public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Duration ttl) {
142+
143+
Assert.notNull(name, "Name must not be null");
144+
Assert.notNull(key, "Key must not be null");
145+
146+
CompletableFuture<byte[]> result = nonBlockingRetrieveFunction(name).apply(key, ttl);
147+
148+
result = result.thenApply(cachedValue -> {
149+
150+
statistics.incGets(name);
151+
152+
if (cachedValue != null) {
153+
statistics.incHits(name);
154+
} else {
155+
statistics.incMisses(name);
156+
}
157+
158+
return cachedValue;
159+
});
160+
161+
return result;
162+
}
163+
164+
private BiFunction<byte[], Duration, CompletableFuture<byte[]>> nonBlockingRetrieveFunction(String cacheName) {
165+
return isReactive() ? reactiveRetrieveFunction(cacheName) : asyncRetrieveFunction(cacheName);
166+
}
167+
168+
// Function applied for Cache.retrieve(key) when a non-reactive Redis driver is used, such as Jedis.
169+
private BiFunction<byte[], Duration, CompletableFuture<byte[]>> asyncRetrieveFunction(String cacheName) {
170+
171+
return (key, ttl) -> {
172+
173+
Supplier<byte[]> getKey = () -> execute(cacheName, connection -> connection.stringCommands().get(key));
174+
175+
Supplier<byte[]> getKeyWithExpiration = () -> execute(cacheName, connection ->
176+
connection.stringCommands().getEx(key, Expiration.from(ttl)));
177+
178+
return shouldExpireWithin(ttl)
179+
? CompletableFuture.supplyAsync(getKeyWithExpiration)
180+
: CompletableFuture.supplyAsync(getKey);
181+
182+
};
183+
}
184+
185+
// Function applied for Cache.retrieve(key) when a reactive Redis driver is used, such as Lettuce.
186+
private BiFunction<byte[], Duration, CompletableFuture<byte[]>> reactiveRetrieveFunction(String cacheName) {
187+
188+
return (key, ttl) -> {
189+
190+
ByteBuffer wrappedKey = ByteBuffer.wrap(key);
191+
192+
Flux<?> cacheLockCheckFlux = Flux.interval(Duration.ZERO, this.sleepTime).takeUntil(count ->
193+
executeLockFree(connection -> !doCheckLock(cacheName, connection)));
194+
195+
Mono<ByteBuffer> getMono = shouldExpireWithin(ttl)
196+
? executeReactively(connection -> connection.stringCommands().getEx(wrappedKey, Expiration.from(ttl)))
197+
: executeReactively(connection -> connection.stringCommands().get(wrappedKey));
198+
199+
Mono<ByteBuffer> result = cacheLockCheckFlux.then(getMono);
200+
201+
@SuppressWarnings("all")
202+
Mono<byte[]> byteArrayResult = result.map(DefaultRedisCacheWriter::nullSafeGetBytes);
203+
204+
return byteArrayResult.toFuture();
205+
};
206+
}
207+
131208
@Override
132209
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
133210

@@ -282,32 +359,42 @@ private Long doUnlock(String name, RedisConnection connection) {
282359
return connection.keyCommands().del(createCacheLockKey(name));
283360
}
284361

285-
boolean doCheckLock(String name, RedisConnection connection) {
286-
return isTrue(connection.keyCommands().exists(createCacheLockKey(name)));
287-
}
362+
private <T> T execute(String name, Function<RedisConnection, T> callback) {
288363

289-
/**
290-
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
291-
*/
292-
private boolean isLockingCacheWriter() {
293-
return !sleepTime.isZero() && !sleepTime.isNegative();
364+
try (RedisConnection connection = this.connectionFactory.getConnection()) {
365+
checkAndPotentiallyWaitUntilUnlocked(name, connection);
366+
return callback.apply(connection);
367+
}
294368
}
295369

296-
private <T> T execute(String name, Function<RedisConnection, T> callback) {
370+
private <T> T executeLockFree(Function<RedisConnection, T> callback) {
297371

298-
try (RedisConnection connection = connectionFactory.getConnection()) {
299-
checkAndPotentiallyWaitUntilUnlocked(name, connection);
372+
try (RedisConnection connection = this.connectionFactory.getConnection()) {
300373
return callback.apply(connection);
301374
}
302375
}
303376

304-
private void executeLockFree(Consumer<RedisConnection> callback) {
377+
private <T> T executeReactively(Function<ReactiveRedisConnection, T> callback) {
378+
379+
ReactiveRedisConnection connection = getReactiveRedisConnectionFactory().getReactiveConnection();
305380

306-
try (RedisConnection connection = connectionFactory.getConnection()) {
307-
callback.accept(connection);
381+
try {
382+
return callback.apply(connection);
383+
}
384+
finally {
385+
connection.closeLater();
308386
}
309387
}
310388

389+
/**
390+
* Determines whether this {@link RedisCacheWriter} uses locks during caching operations.
391+
*
392+
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
393+
*/
394+
private boolean isLockingCacheWriter() {
395+
return !this.sleepTime.isZero() && !this.sleepTime.isNegative();
396+
}
397+
311398
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
312399

313400
if (!isLockingCacheWriter()) {
@@ -318,29 +405,46 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c
318405

319406
try {
320407
while (doCheckLock(name, connection)) {
321-
Thread.sleep(sleepTime.toMillis());
408+
Thread.sleep(this.sleepTime.toMillis());
322409
}
323410
} catch (InterruptedException cause) {
324411

325-
// Re-interrupt current thread, to allow other participants to react.
412+
// Re-interrupt current Thread to allow other participants to react.
326413
Thread.currentThread().interrupt();
327414

328415
String message = String.format("Interrupted while waiting to unlock cache %s", name);
329416

330417
throw new PessimisticLockingFailureException(message, cause);
331418
} finally {
332-
statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs);
419+
this.statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs);
333420
}
334421
}
335422

423+
boolean doCheckLock(String name, RedisConnection connection) {
424+
return isTrue(connection.keyCommands().exists(createCacheLockKey(name)));
425+
}
426+
427+
private boolean isReactive() {
428+
return this.connectionFactory instanceof ReactiveRedisConnectionFactory;
429+
}
430+
431+
private ReactiveRedisConnectionFactory getReactiveRedisConnectionFactory() {
432+
return (ReactiveRedisConnectionFactory) this.connectionFactory;
433+
}
434+
336435
private static byte[] createCacheLockKey(String name) {
337436
return (name + "~lock").getBytes(StandardCharsets.UTF_8);
338437
}
339438

340-
private boolean isTrue(@Nullable Boolean value) {
439+
private static boolean isTrue(@Nullable Boolean value) {
341440
return Boolean.TRUE.equals(value);
342441
}
343442

443+
@Nullable
444+
private static byte[] nullSafeGetBytes(@Nullable ByteBuffer value) {
445+
return value != null ? ByteUtils.getBytes(value) : null;
446+
}
447+
344448
private static boolean shouldExpireWithin(@Nullable Duration ttl) {
345449
return ttl != null && !ttl.isZero() && !ttl.isNegative();
346450
}

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

+19-4
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import org.springframework.util.ReflectionUtils;
4747

4848
/**
49-
* {@link org.springframework.cache.Cache} implementation using for Redis as the underlying store for cache data.
49+
* {@link AbstractValueAdaptingCache Cache} implementation using Redis as the underlying store for cache data.
5050
* <p>
5151
* Use {@link RedisCacheManager} to create {@link RedisCache} instances.
5252
*
@@ -61,7 +61,7 @@
6161
@SuppressWarnings("unused")
6262
public class RedisCache extends AbstractValueAdaptingCache {
6363

64-
private static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE);
64+
static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE);
6565

6666
private final Lock lock = new ReentrantLock();
6767

@@ -293,12 +293,27 @@ protected Object preProcessCacheValue(@Nullable Object value) {
293293

294294
@Override
295295
public CompletableFuture<?> retrieve(Object key) {
296-
return super.retrieve(key);
296+
return retrieveValue(key).thenApply(this::nullSafeDeserializedStoreValue);
297297
}
298298

299299
@Override
300+
@SuppressWarnings("unchecked")
300301
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
301-
return super.retrieve(key, valueLoader);
302+
303+
return retrieveValue(key)
304+
.thenApply(this::nullSafeDeserializedStoreValue)
305+
.thenCompose(cachedValue -> cachedValue != null
306+
? CompletableFuture.completedFuture((T) cachedValue)
307+
: valueLoader.get());
308+
}
309+
310+
CompletableFuture<byte[]> retrieveValue(Object key) {
311+
return getCacheWriter().retrieve(getName(), createAndConvertCacheKey(key));
312+
}
313+
314+
@Nullable
315+
Object nullSafeDeserializedStoreValue(@Nullable byte[] value) {
316+
return value != null ? fromStoreValue(deserializeCacheValue(value)) : null;
302317
}
303318

304319
/**

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

+30
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.redis.cache;
1717

1818
import java.time.Duration;
19+
import java.util.concurrent.CompletableFuture;
1920

2021
import org.springframework.data.redis.connection.RedisConnectionFactory;
2122
import org.springframework.lang.Nullable;
@@ -135,6 +136,35 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) {
135136
return get(name, key);
136137
}
137138

139+
/**
140+
* Returns the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}.
141+
* <p>
142+
* This operation is non-blocking.
143+
*
144+
* @param name {@link String} with the name of the {@link RedisCache}.
145+
* @param key {@link byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}.
146+
* @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}.
147+
* @see #retrieve(String, byte[], Duration)
148+
* @since 3.2.0
149+
*/
150+
default CompletableFuture<byte[]> retrieve(String name, byte[] key) {
151+
return retrieve(name, key, null);
152+
}
153+
154+
/**
155+
* Returns the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}
156+
* setting the {@link Duration TTL expiration} for the cache entry.
157+
* <p>
158+
* This operation is non-blocking.
159+
*
160+
* @param name {@link String} with the name of the {@link RedisCache}.
161+
* @param key {@link byte[] key} mapped to the {@link CompletableFuture value} in the {@link RedisCache}.
162+
* @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry.
163+
* @return the {@link CompletableFuture value} to which the {@link RedisCache} maps the given {@link byte[] key}.
164+
* @since 3.2.0
165+
*/
166+
CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Duration ttl);
167+
138168
/**
139169
* Write the given key/value pair to Redis and set the expiration time if defined.
140170
*

0 commit comments

Comments
 (0)