Skip to content

Commit 35df601

Browse files
committed
Refine and clarify operations in asynchronous caching implementation.
Uses more descriptive names for operations, especially Reactive operations, by either calling a local, private method or introducing a (functional) variable with strongly typed parameters. Edits and refines Javadoc. Original Pull Request: #2717 Closes #2743
1 parent 9a81e6c commit 35df601

File tree

1 file changed

+109
-72
lines changed

1 file changed

+109
-72
lines changed

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

+109-72
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717

1818
import reactor.core.publisher.Flux;
1919
import reactor.core.publisher.Mono;
20+
import reactor.core.publisher.SignalType;
2021

2122
import java.nio.ByteBuffer;
2223
import java.nio.charset.StandardCharsets;
2324
import java.time.Duration;
2425
import java.util.concurrent.CompletableFuture;
2526
import java.util.concurrent.TimeUnit;
2627
import java.util.concurrent.atomic.AtomicLong;
28+
import java.util.function.Consumer;
2729
import java.util.function.Function;
2830

2931
import org.springframework.dao.PessimisticLockingFailureException;
@@ -38,7 +40,7 @@
3840
import org.springframework.lang.Nullable;
3941
import org.springframework.util.Assert;
4042
import org.springframework.util.ClassUtils;
41-
import org.springframework.util.ObjectUtils;
43+
4244

4345
/**
4446
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
@@ -47,11 +49,11 @@
4749
* <p>
4850
* {@link DefaultRedisCacheWriter} can be used in
4951
* {@link RedisCacheWriter#lockingRedisCacheWriter(RedisConnectionFactory) locking} or
50-
* {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While
51-
* {@literal non-locking} aims for maximum performance it may result in overlapping, non-atomic, command execution for
52-
* operations spanning multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents
53-
* command overlap by setting an explicit lock key and checking against presence of this key which leads to additional
54-
* requests and potential command wait times.
52+
* {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking}
53+
* aims for maximum performance it may result in overlapping, non-atomic, command execution for operations spanning
54+
* multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap
55+
* by setting an explicit lock key and checking against presence of this key which leads to additional requests
56+
* and potential command wait times.
5557
*
5658
* @author Christoph Strobl
5759
* @author Mark Paluch
@@ -61,8 +63,12 @@
6163
*/
6264
class DefaultRedisCacheWriter implements RedisCacheWriter {
6365

64-
private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT = ClassUtils
65-
.isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null);
66+
public static final boolean FLUX_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Flux", null);
67+
68+
private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT =
69+
ClassUtils.isPresent("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory", null);
70+
71+
private final AsyncCacheWriter asyncCacheWriter;
6672

6773
private final BatchStrategy batchStrategy;
6874

@@ -74,8 +80,6 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
7480

7581
private final TtlFunction lockTtl;
7682

77-
private final AsyncCacheWriter asyncCacheWriter;
78-
7983
/**
8084
* @param connectionFactory must not be {@literal null}.
8185
* @param batchStrategy must not be {@literal null}.
@@ -86,8 +90,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
8690

8791
/**
8892
* @param connectionFactory must not be {@literal null}.
89-
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
90-
* to disable locking.
93+
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}.
94+
* Use {@link Duration#ZERO} to disable locking.
9195
* @param batchStrategy must not be {@literal null}.
9296
*/
9397
DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime, BatchStrategy batchStrategy) {
@@ -96,8 +100,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
96100

97101
/**
98102
* @param connectionFactory must not be {@literal null}.
99-
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
100-
* to disable locking.
103+
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}.
104+
* Use {@link Duration#ZERO} to disable locking.
101105
* @param lockTtl Lock TTL function must not be {@literal null}.
102106
* @param cacheStatisticsCollector must not be {@literal null}.
103107
* @param batchStrategy must not be {@literal null}.
@@ -116,12 +120,13 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
116120
this.lockTtl = lockTtl;
117121
this.statistics = cacheStatisticsCollector;
118122
this.batchStrategy = batchStrategy;
123+
this.asyncCacheWriter = isAsyncCacheSupportEnabled() ? new AsynchronousCacheWriterDelegate()
124+
: UnsupportedAsyncCacheWriter.INSTANCE;
125+
}
119126

120-
if (REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT && this.connectionFactory instanceof ReactiveRedisConnectionFactory) {
121-
asyncCacheWriter = new AsynchronousCacheWriterDelegate();
122-
} else {
123-
asyncCacheWriter = UnsupportedAsyncCacheWriter.INSTANCE;
124-
}
127+
private boolean isAsyncCacheSupportEnabled() {
128+
return REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT && FLUX_PRESENT
129+
&& this.connectionFactory instanceof ReactiveRedisConnectionFactory;
125130
}
126131

127132
@Override
@@ -168,7 +173,8 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
168173

169174
if (cachedValue != null) {
170175
statistics.incHits(name);
171-
} else {
176+
}
177+
else {
172178
statistics.incMisses(name);
173179
}
174180

@@ -186,8 +192,7 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
186192
execute(name, connection -> {
187193

188194
if (shouldExpireWithin(ttl)) {
189-
connection.stringCommands().set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS),
190-
SetOption.upsert());
195+
connection.stringCommands().set(key, value, toExpiration(ttl), SetOption.upsert());
191196
} else {
192197
connection.stringCommands().set(key, value);
193198
}
@@ -224,16 +229,11 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat
224229

225230
try {
226231

227-
boolean put;
228-
229-
if (shouldExpireWithin(ttl)) {
230-
put = ObjectUtils.nullSafeEquals(
231-
connection.stringCommands().set(key, value, Expiration.from(ttl), SetOption.ifAbsent()), true);
232-
} else {
233-
put = ObjectUtils.nullSafeEquals(connection.stringCommands().setNX(key, value), true);
234-
}
232+
Boolean wasSet = shouldExpireWithin(ttl)
233+
? connection.stringCommands().set(key, value, Expiration.from(ttl), SetOption.ifAbsent())
234+
: connection.stringCommands().setNX(key, value);
235235

236-
if (put) {
236+
if (Boolean.TRUE.equals(wasSet)) {
237237
statistics.incPuts(name);
238238
return null;
239239
}
@@ -322,9 +322,11 @@ void lock(String name) {
322322
private Boolean doLock(String name, Object contextualKey, @Nullable Object contextualValue,
323323
RedisConnection connection) {
324324

325-
Expiration expiration = Expiration.from(this.lockTtl.getTimeToLive(contextualKey, contextualValue));
325+
byte[] cacheLockKey = createCacheLockKey(name);
326326

327-
return connection.stringCommands().set(createCacheLockKey(name), new byte[0], expiration, SetOption.SET_IF_ABSENT);
327+
Expiration expiration = toExpiration(contextualKey, contextualValue);
328+
329+
return connection.stringCommands().set(cacheLockKey, new byte[0], expiration, SetOption.SET_IF_ABSENT);
328330
}
329331

330332
/**
@@ -378,29 +380,40 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c
378380
Thread.sleep(this.sleepTime.toMillis());
379381
}
380382
} catch (InterruptedException cause) {
381-
382383
// Re-interrupt current Thread to allow other participants to react.
383384
Thread.currentThread().interrupt();
384-
385-
throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
386-
cause);
385+
String message = "Interrupted while waiting to unlock cache %s".formatted(name);
386+
throw new PessimisticLockingFailureException(message, cause);
387387
} finally {
388388
this.statistics.incLockTime(name, System.nanoTime() - lockWaitTimeNs);
389389
}
390390
}
391391

392392
boolean doCheckLock(String name, RedisConnection connection) {
393-
return ObjectUtils.nullSafeEquals(connection.keyCommands().exists(createCacheLockKey(name)), true);
393+
Boolean cacheLockExists = connection.keyCommands().exists(createCacheLockKey(name));
394+
return Boolean.TRUE.equals(cacheLockExists);
394395
}
395396

396397
byte[] createCacheLockKey(String name) {
397398
return (name + "~lock").getBytes(StandardCharsets.UTF_8);
398399
}
399400

401+
private ReactiveRedisConnectionFactory getReactiveConnectionFactory() {
402+
return (ReactiveRedisConnectionFactory) this.connectionFactory;
403+
}
404+
400405
private static boolean shouldExpireWithin(@Nullable Duration ttl) {
401406
return ttl != null && !ttl.isZero() && !ttl.isNegative();
402407
}
403408

409+
private Expiration toExpiration(Duration ttl) {
410+
return Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS);
411+
}
412+
413+
private Expiration toExpiration(Object key, @Nullable Object value) {
414+
return Expiration.from(this.lockTtl.getTimeToLive(key, value));
415+
}
416+
404417
/**
405418
* Interface for asynchronous cache retrieval.
406419
*
@@ -419,8 +432,8 @@ interface AsyncCacheWriter {
419432
* @param name the cache name from which to retrieve the cache entry.
420433
* @param key the cache entry key.
421434
* @param ttl optional TTL to set for Time-to-Idle eviction.
422-
* @return a future that completes either with a value if the value exists or completing with {@code null} if the
423-
* cache does not contain an entry.
435+
* @return a future that completes either with a value if the value exists or completing with {@code null}
436+
* if the cache does not contain an entry.
424437
*/
425438
CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Duration ttl);
426439

@@ -463,8 +476,8 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
463476
}
464477

465478
/**
466-
* Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations using
467-
* {@link ReactiveRedisConnectionFactory}.
479+
* Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations
480+
* using {@link ReactiveRedisConnectionFactory}.
468481
*
469482
* @since 3.2
470483
*/
@@ -481,11 +494,13 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
481494
return doWithConnection(connection -> {
482495

483496
ByteBuffer wrappedKey = ByteBuffer.wrap(key);
497+
484498
Mono<?> cacheLockCheck = isLockingCacheWriter() ? waitForLock(connection, name) : Mono.empty();
499+
485500
ReactiveStringCommands stringCommands = connection.stringCommands();
486501

487502
Mono<ByteBuffer> get = shouldExpireWithin(ttl)
488-
? stringCommands.getEx(wrappedKey, Expiration.from(ttl))
503+
? stringCommands.getEx(wrappedKey, toExpiration(ttl))
489504
: stringCommands.get(wrappedKey);
490505

491506
return cacheLockCheck.then(get).map(ByteUtils::getBytes).toFuture();
@@ -498,75 +513,97 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
498513
return doWithConnection(connection -> {
499514

500515
Mono<?> mono = isLockingCacheWriter()
501-
? doStoreWithLocking(name, key, value, ttl, connection)
516+
? doLockStoreUnlock(name, key, value, ttl, connection)
502517
: doStore(key, value, ttl, connection);
503518

504519
return mono.then().toFuture();
505520
});
506521
}
507522

508-
private Mono<Boolean> doStoreWithLocking(String name, byte[] key, byte[] value, @Nullable Duration ttl,
509-
ReactiveRedisConnection connection) {
510-
511-
return Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection),
512-
unused -> doUnlock(name, connection));
513-
}
514-
515523
private Mono<Boolean> doStore(byte[] cacheKey, byte[] value, @Nullable Duration ttl,
516524
ReactiveRedisConnection connection) {
517525

518526
ByteBuffer wrappedKey = ByteBuffer.wrap(cacheKey);
519527
ByteBuffer wrappedValue = ByteBuffer.wrap(value);
520528

521-
if (shouldExpireWithin(ttl)) {
522-
return connection.stringCommands().set(wrappedKey, wrappedValue,
523-
Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
524-
} else {
525-
return connection.stringCommands().set(wrappedKey, wrappedValue);
526-
}
529+
ReactiveStringCommands stringCommands = connection.stringCommands();
530+
531+
return shouldExpireWithin(ttl)
532+
? stringCommands.set(wrappedKey, wrappedValue, toExpiration(ttl), SetOption.upsert())
533+
: stringCommands.set(wrappedKey, wrappedValue);
527534
}
528535

536+
private Mono<Boolean> doLockStoreUnlock(String name, byte[] key, byte[] value, @Nullable Duration ttl,
537+
ReactiveRedisConnection connection) {
538+
539+
Mono<Object> lock = doLock(name, key, value, connection);
540+
541+
Function<Object, Mono<Boolean>> store = unused -> doStore(key, value, ttl, connection);
542+
Function<Object, Mono<Void>> unlock = unused -> doUnlock(name, connection);
543+
544+
return Mono.usingWhen(lock, store, unlock);
545+
}
529546

530547
private Mono<Object> doLock(String name, Object contextualKey, @Nullable Object contextualValue,
531548
ReactiveRedisConnection connection) {
532549

533-
ByteBuffer key = ByteBuffer.wrap(createCacheLockKey(name));
550+
ByteBuffer key = toCacheLockKey(name);
534551
ByteBuffer value = ByteBuffer.wrap(new byte[0]);
535-
Expiration expiration = Expiration.from(lockTtl.getTimeToLive(contextualKey, contextualValue));
552+
553+
Expiration expiration = toExpiration(contextualKey, contextualValue);
536554

537555
return connection.stringCommands().set(key, value, expiration, SetOption.SET_IF_ABSENT) //
538556
// Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run the inner resource function.
539557
.thenReturn(Boolean.TRUE);
540558
}
541559

542560
private Mono<Void> doUnlock(String name, ReactiveRedisConnection connection) {
543-
return connection.keyCommands().del(ByteBuffer.wrap(createCacheLockKey(name))).then();
561+
return connection.keyCommands().del(toCacheLockKey(name)).then();
544562
}
545563

546564
private Mono<Void> waitForLock(ReactiveRedisConnection connection, String cacheName) {
547565

548-
AtomicLong lockWaitTimeNs = new AtomicLong();
549-
byte[] cacheLockKey = createCacheLockKey(cacheName);
566+
AtomicLong lockWaitNanoTime = new AtomicLong();
567+
568+
Consumer<org.reactivestreams.Subscription> setNanoTimeOnLockWait = subscription ->
569+
lockWaitNanoTime.set(System.nanoTime());
550570

551-
Flux<Long> wait = Flux.interval(Duration.ZERO, sleepTime);
552-
Mono<Boolean> exists = connection.keyCommands().exists(ByteBuffer.wrap(cacheLockKey)).filter(it -> !it);
571+
Consumer<SignalType> recordStatistics = signalType ->
572+
statistics.incLockTime(cacheName, System.nanoTime() - lockWaitNanoTime.get());
553573

554-
return wait.doOnSubscribe(subscription -> lockWaitTimeNs.set(System.nanoTime())) //
555-
.flatMap(it -> exists) //
556-
.doFinally(signalType -> statistics.incLockTime(cacheName, System.nanoTime() - lockWaitTimeNs.get())) //
574+
Function<Long, Mono<Boolean>> doWhileCacheLockExists = lockWaitTime -> connection.keyCommands()
575+
.exists(toCacheLockKey(cacheName)).filter(cacheLockKeyExists -> !cacheLockKeyExists);
576+
577+
return waitInterval(sleepTime) //
578+
.doOnSubscribe(setNanoTimeOnLockWait) //
579+
.flatMap(doWhileCacheLockExists) //
580+
.doFinally(recordStatistics) //
557581
.next() //
558582
.then();
559583
}
560584

585+
private Flux<Long> waitInterval(Duration period) {
586+
return Flux.interval(Duration.ZERO, period);
587+
}
588+
589+
private ByteBuffer toCacheLockKey(String cacheName) {
590+
return ByteBuffer.wrap(createCacheLockKey(cacheName));
591+
}
592+
561593
private <T> CompletableFuture<T> doWithConnection(
562594
Function<ReactiveRedisConnection, CompletableFuture<T>> callback) {
563595

564-
ReactiveRedisConnectionFactory cf = (ReactiveRedisConnectionFactory) connectionFactory;
596+
Mono<ReactiveRedisConnection> reactiveConnection =
597+
Mono.fromSupplier(getReactiveConnectionFactory()::getReactiveConnection);
598+
599+
Function<ReactiveRedisConnection, Mono<T>> commandExecution = connection ->
600+
Mono.fromCompletionStage(callback.apply(connection));
601+
602+
Function<ReactiveRedisConnection, Mono<Void>> connectionClose = ReactiveRedisConnection::closeLater;
603+
604+
Mono<T> result = Mono.usingWhen(reactiveConnection, commandExecution, connectionClose);
565605

566-
return Mono.usingWhen(Mono.fromSupplier(cf::getReactiveConnection), //
567-
it -> Mono.fromCompletionStage(callback.apply(it)), //
568-
ReactiveRedisConnection::closeLater) //
569-
.toFuture();
606+
return result.toFuture();
570607
}
571608
}
572609
}

0 commit comments

Comments
 (0)