Skip to content

Commit d87815f

Browse files
committed
Implement retrieve(key) and retrieve(key, :Supplier<CompletableFuture<T>>) operations in RedisCache.
Closes #2650
1 parent 624d7c6 commit d87815f

File tree

6 files changed

+537
-58
lines changed

6 files changed

+537
-58
lines changed

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

Lines changed: 103 additions & 3 deletions
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;
23+
import java.util.function.BiFunction;
2124
import java.util.function.Consumer;
2225
import java.util.function.Function;
26+
import java.util.function.Supplier;
2327

2428
import org.springframework.dao.PessimisticLockingFailureException;
29+
import org.springframework.data.redis.connection.ReactiveRedisConnection;
30+
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
2531
import org.springframework.data.redis.connection.RedisConnection;
2632
import org.springframework.data.redis.connection.RedisConnectionFactory;
2733
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
2834
import org.springframework.data.redis.core.types.Expiration;
35+
import org.springframework.data.redis.util.ByteUtils;
2936
import org.springframework.lang.Nullable;
3037
import org.springframework.util.Assert;
3138

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,77 @@ public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
128137
return result;
129138
}
130139

140+
@Override
141+
public Mono<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+
Mono<byte[]> result = nonBlockingExecutionStrategy(name).apply(key, ttl);
147+
148+
result = result.doOnSuccess(byteBuffer -> {
149+
if (byteBuffer != null) {
150+
statistics.incHits(name);
151+
}
152+
else {
153+
statistics.incMisses(name);
154+
}
155+
}).doFirst(() -> statistics.incGets(name));
156+
157+
return result;
158+
}
159+
160+
private BiFunction<byte[], Duration, Mono<byte[]>> nonBlockingExecutionStrategy(String cacheName) {
161+
return isReactiveAvailable() ? reactiveExecutionStrategy(cacheName) : asyncExecutionStrategy(cacheName);
162+
}
163+
164+
// Execution Strategy applied when Jedis (non-Reactive driver) is used.
165+
// Treats API consistently (that is, using Reactive types, such as Mono) at the RedisCacheWriter level
166+
// whether "technically Reactive" or not; clearly Jedis is not "Reactive".
167+
private BiFunction<byte[], Duration, Mono<byte[]>> asyncExecutionStrategy(String cacheName) {
168+
169+
return (key, ttl) -> {
170+
171+
Supplier<byte[]> getKey = () -> execute(cacheName, connection -> connection.stringCommands().get(key));
172+
173+
Supplier<byte[]> getKeyWithExpiration = () -> execute(cacheName, connection ->
174+
connection.stringCommands().getEx(key, Expiration.from(ttl)));
175+
176+
// NOTE: CompletableFuture.supplyAsync(:Supplier) is necessary in this case to prevent blocking
177+
// on Mono.subscribe(:Consumer).
178+
return shouldExpireWithin(ttl)
179+
? Mono.fromFuture(CompletableFuture.supplyAsync(getKeyWithExpiration))
180+
: Mono.fromFuture(CompletableFuture.supplyAsync(getKey));
181+
};
182+
}
183+
184+
// Execution Strategy applied when Lettuce (Reactive driver) is used.
185+
// Be careful to do this in a "non-blocking way", but still taking the "named" cache lock into consideration.
186+
private BiFunction<byte[], Duration, Mono<byte[]>> reactiveExecutionStrategy(String cacheName) {
187+
188+
return (key, ttl) -> {
189+
190+
ByteBuffer wrappedKey = ByteBuffer.wrap(key);
191+
192+
Mono<ByteBuffer> result = shouldExpireWithin(ttl)
193+
? executeReactively(connection -> connection.stringCommands().getEx(wrappedKey, Expiration.from(ttl)))
194+
: executeReactively(connection -> connection.stringCommands().get(wrappedKey));
195+
196+
// Do the same lock check as the regular Cache.get(key); be careful of blocking!
197+
result = result.doFirst(() -> executeLockFree(connection ->
198+
checkAndPotentiallyWaitUntilUnlocked(cacheName, connection)));
199+
200+
@SuppressWarnings("all")
201+
Mono<byte[]> byteArrayResult = result.map(DefaultRedisCacheWriter::nullSafeGetBytes);
202+
203+
return byteArrayResult;
204+
};
205+
}
206+
207+
@Nullable
208+
private static byte[] nullSafeGetBytes(@Nullable ByteBuffer value) {
209+
return value != null ? ByteUtils.getBytes(value) : null;
210+
}
131211
@Override
132212
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
133213

@@ -308,6 +388,18 @@ private void executeLockFree(Consumer<RedisConnection> callback) {
308388
}
309389
}
310390

391+
private <T> T executeReactively(Function<ReactiveRedisConnection, T> callback) {
392+
393+
ReactiveRedisConnection connection = getReactiveRedisConnectionFactory().getReactiveConnection();
394+
395+
try {
396+
return callback.apply(connection);
397+
}
398+
finally {
399+
connection.closeLater();
400+
}
401+
}
402+
311403
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
312404

313405
if (!isLockingCacheWriter()) {
@@ -333,11 +425,19 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c
333425
}
334426
}
335427

428+
private boolean isReactiveAvailable() {
429+
return this.connectionFactory instanceof ReactiveRedisConnectionFactory;
430+
}
431+
432+
private ReactiveRedisConnectionFactory getReactiveRedisConnectionFactory() {
433+
return (ReactiveRedisConnectionFactory) this.connectionFactory;
434+
}
435+
336436
private static byte[] createCacheLockKey(String name) {
337437
return (name + "~lock").getBytes(StandardCharsets.UTF_8);
338438
}
339439

340-
private boolean isTrue(@Nullable Boolean value) {
440+
private static boolean isTrue(@Nullable Boolean value) {
341441
return Boolean.TRUE.equals(value);
342442
}
343443

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18+
import reactor.core.publisher.Mono;
19+
1820
import java.lang.reflect.Method;
1921
import java.nio.ByteBuffer;
2022
import java.time.Duration;
@@ -46,7 +48,7 @@
4648
import org.springframework.util.ReflectionUtils;
4749

4850
/**
49-
* {@link org.springframework.cache.Cache} implementation using for Redis as the underlying store for cache data.
51+
* {@link AbstractValueAdaptingCache Cache} implementation using Redis as the underlying store for cache data.
5052
* <p>
5153
* Use {@link RedisCacheManager} to create {@link RedisCache} instances.
5254
*
@@ -293,12 +295,24 @@ protected Object preProcessCacheValue(@Nullable Object value) {
293295

294296
@Override
295297
public CompletableFuture<?> retrieve(Object key) {
296-
return super.retrieve(key);
298+
299+
return retrieveMono(key)
300+
.map(this::deserializeCacheValue)
301+
.toFuture();
297302
}
298303

299304
@Override
305+
@SuppressWarnings("unchecked")
300306
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
301-
return super.retrieve(key, valueLoader);
307+
308+
return retrieveMono(key)
309+
.map(value -> (T) deserializeCacheValue(value))
310+
.switchIfEmpty(Mono.fromCompletionStage(valueLoader))
311+
.toFuture();
312+
}
313+
314+
private Mono<byte[]> retrieveMono(Object key) {
315+
return getCacheWriter().retrieve(getName(), createAndConvertCacheKey(key));
302316
}
303317

304318
/**

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.springframework.lang.Nullable;
2222
import org.springframework.util.Assert;
2323

24+
import reactor.core.publisher.Mono;
25+
2426
/**
2527
* {@link RedisCacheWriter} provides low-level access to Redis commands ({@code SET, SETNX, GET, EXPIRE,...}) used for
2628
* caching.
@@ -135,6 +137,40 @@ default byte[] get(String name, byte[] key, @Nullable Duration ttl) {
135137
return get(name, key);
136138
}
137139

140+
/**
141+
* Returns the {@link Mono value} to which the {@link RedisCache} maps the given {@link byte[] key}.
142+
* <p>
143+
* This operation does not block.
144+
*
145+
* @param name {@link String} containing the name of the {@link RedisCache}.
146+
* @param key {@link byte[] key} mapped to the {@link Mono value} in the {@link RedisCache}.
147+
* @return the {@link Mono value} to which the {@link RedisCache} maps the given {@link byte[] key}.
148+
* @throws IllegalStateException if the Redis connection factory is not reactive.
149+
* @see reactor.core.publisher.Mono
150+
* @see java.nio.ByteBuffer
151+
* @since 3.2.0
152+
*/
153+
default Mono<byte[]> retrieve(String name, byte[] key) {
154+
return retrieve(name, key, null);
155+
}
156+
157+
/**
158+
* Returns the {@link Mono value} to which the {@link RedisCache} maps the given {@link byte[] key}
159+
* setting the {@link Duration TTL expiration} for the cache entry.
160+
* <p>
161+
* This operation does not block.
162+
*
163+
* @param name {@link String} containing the name of the {@link RedisCache}.
164+
* @param key {@link byte[] key} mapped to the {@link Mono value} in the {@link RedisCache}.
165+
* @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry.
166+
* @return the {@link Mono value} to which the {@link RedisCache} maps the given {@link byte[] key}.
167+
* @throws IllegalStateException if the Redis connection factory is not reactive.
168+
* @see reactor.core.publisher.Mono
169+
* @see java.nio.ByteBuffer
170+
* @since 3.2.0
171+
*/
172+
Mono<byte[]> retrieve(String name, byte[] key, @Nullable Duration ttl);
173+
138174
/**
139175
* Write the given key/value pair to Redis and set the expiration time if defined.
140176
*

0 commit comments

Comments
 (0)