15
15
*/
16
16
package org .springframework .data .redis .cache ;
17
17
18
- import reactor .core .publisher .Flux ;
19
- import reactor .core .publisher .Mono ;
20
-
21
18
import java .nio .ByteBuffer ;
22
19
import java .nio .charset .StandardCharsets ;
23
20
import java .time .Duration ;
24
21
import java .util .concurrent .CompletableFuture ;
25
22
import java .util .concurrent .TimeUnit ;
26
23
import java .util .concurrent .atomic .AtomicLong ;
24
+ import java .util .function .Consumer ;
27
25
import java .util .function .Function ;
28
26
29
27
import org .springframework .dao .PessimisticLockingFailureException ;
38
36
import org .springframework .lang .Nullable ;
39
37
import org .springframework .util .Assert ;
40
38
import org .springframework .util .ClassUtils ;
41
- import org .springframework .util .ObjectUtils ;
39
+
40
+ import reactor .core .publisher .Flux ;
41
+ import reactor .core .publisher .Mono ;
42
+ import reactor .core .publisher .SignalType ;
42
43
43
44
/**
44
45
* {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
47
48
* <p>
48
49
* {@link DefaultRedisCacheWriter} can be used in
49
50
* {@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.
51
+ * {@link RedisCacheWriter#nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking}
52
+ * aims for maximum performance it may result in overlapping, non-atomic, command execution for operations spanning
53
+ * multiple Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap
54
+ * by setting an explicit lock key and checking against presence of this key which leads to additional requests
55
+ * and potential command wait times.
55
56
*
56
57
* @author Christoph Strobl
57
58
* @author Mark Paluch
61
62
*/
62
63
class DefaultRedisCacheWriter implements RedisCacheWriter {
63
64
64
- private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT = ClassUtils
65
- .isPresent ("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory" , null );
65
+ public static final boolean FLUX_PRESENT = ClassUtils .isPresent ("reactor.core.publisher.Flux" , null );
66
+
67
+ private static final boolean REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT =
68
+ ClassUtils .isPresent ("org.springframework.data.redis.connection.ReactiveRedisConnectionFactory" , null );
69
+
70
+ private final AsyncCacheWriter asyncCacheWriter ;
66
71
67
72
private final BatchStrategy batchStrategy ;
68
73
@@ -74,8 +79,6 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
74
79
75
80
private final TtlFunction lockTtl ;
76
81
77
- private final AsyncCacheWriter asyncCacheWriter ;
78
-
79
82
/**
80
83
* @param connectionFactory must not be {@literal null}.
81
84
* @param batchStrategy must not be {@literal null}.
@@ -86,8 +89,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
86
89
87
90
/**
88
91
* @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.
92
+ * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}.
93
+ * Use {@link Duration#ZERO} to disable locking.
91
94
* @param batchStrategy must not be {@literal null}.
92
95
*/
93
96
DefaultRedisCacheWriter (RedisConnectionFactory connectionFactory , Duration sleepTime , BatchStrategy batchStrategy ) {
@@ -96,8 +99,8 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
96
99
97
100
/**
98
101
* @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.
102
+ * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}.
103
+ * Use {@link Duration#ZERO} to disable locking.
101
104
* @param lockTtl Lock TTL function must not be {@literal null}.
102
105
* @param cacheStatisticsCollector must not be {@literal null}.
103
106
* @param batchStrategy must not be {@literal null}.
@@ -116,12 +119,13 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
116
119
this .lockTtl = lockTtl ;
117
120
this .statistics = cacheStatisticsCollector ;
118
121
this .batchStrategy = batchStrategy ;
122
+ this .asyncCacheWriter = isAsyncCacheSupportEnabled () ? new AsynchronousCacheWriterDelegate ()
123
+ : UnsupportedAsyncCacheWriter .INSTANCE ;
124
+ }
119
125
120
- if (REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT && this .connectionFactory instanceof ReactiveRedisConnectionFactory ) {
121
- asyncCacheWriter = new AsynchronousCacheWriterDelegate ();
122
- } else {
123
- asyncCacheWriter = UnsupportedAsyncCacheWriter .INSTANCE ;
124
- }
126
+ private boolean isAsyncCacheSupportEnabled () {
127
+ return REACTIVE_REDIS_CONNECTION_FACTORY_PRESENT && FLUX_PRESENT
128
+ && this .connectionFactory instanceof ReactiveRedisConnectionFactory ;
125
129
}
126
130
127
131
@ Override
@@ -168,7 +172,8 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
168
172
169
173
if (cachedValue != null ) {
170
174
statistics .incHits (name );
171
- } else {
175
+ }
176
+ else {
172
177
statistics .incMisses (name );
173
178
}
174
179
@@ -186,8 +191,7 @@ public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
186
191
execute (name , connection -> {
187
192
188
193
if (shouldExpireWithin (ttl )) {
189
- connection .stringCommands ().set (key , value , Expiration .from (ttl .toMillis (), TimeUnit .MILLISECONDS ),
190
- SetOption .upsert ());
194
+ connection .stringCommands ().set (key , value , toExpiration (ttl ), SetOption .upsert ());
191
195
} else {
192
196
connection .stringCommands ().set (key , value );
193
197
}
@@ -224,16 +228,11 @@ public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Durat
224
228
225
229
try {
226
230
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
- }
231
+ Boolean wasSet = shouldExpireWithin (ttl )
232
+ ? connection .stringCommands ().set (key , value , Expiration .from (ttl ), SetOption .ifAbsent ())
233
+ : connection .stringCommands ().setNX (key , value );
235
234
236
- if (put ) {
235
+ if (Boolean . TRUE . equals ( wasSet ) ) {
237
236
statistics .incPuts (name );
238
237
return null ;
239
238
}
@@ -322,9 +321,11 @@ void lock(String name) {
322
321
private Boolean doLock (String name , Object contextualKey , @ Nullable Object contextualValue ,
323
322
RedisConnection connection ) {
324
323
325
- Expiration expiration = Expiration . from ( this . lockTtl . getTimeToLive ( contextualKey , contextualValue ) );
324
+ byte [] cacheLockKey = createCacheLockKey ( name );
326
325
327
- return connection .stringCommands ().set (createCacheLockKey (name ), new byte [0 ], expiration , SetOption .SET_IF_ABSENT );
326
+ Expiration expiration = toExpiration (contextualKey , contextualValue );
327
+
328
+ return connection .stringCommands ().set (cacheLockKey , new byte [0 ], expiration , SetOption .SET_IF_ABSENT );
328
329
}
329
330
330
331
/**
@@ -378,29 +379,40 @@ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection c
378
379
Thread .sleep (this .sleepTime .toMillis ());
379
380
}
380
381
} catch (InterruptedException cause ) {
381
-
382
382
// Re-interrupt current Thread to allow other participants to react.
383
383
Thread .currentThread ().interrupt ();
384
-
385
- throw new PessimisticLockingFailureException (String .format ("Interrupted while waiting to unlock cache %s" , name ),
386
- cause );
384
+ String message = "Interrupted while waiting to unlock cache %s" .formatted (name );
385
+ throw new PessimisticLockingFailureException (message , cause );
387
386
} finally {
388
387
this .statistics .incLockTime (name , System .nanoTime () - lockWaitTimeNs );
389
388
}
390
389
}
391
390
392
391
boolean doCheckLock (String name , RedisConnection connection ) {
393
- return ObjectUtils .nullSafeEquals (connection .keyCommands ().exists (createCacheLockKey (name )), true );
392
+ Boolean cacheLockExists = connection .keyCommands ().exists (createCacheLockKey (name ));
393
+ return Boolean .TRUE .equals (cacheLockExists );
394
394
}
395
395
396
396
byte [] createCacheLockKey (String name ) {
397
397
return (name + "~lock" ).getBytes (StandardCharsets .UTF_8 );
398
398
}
399
399
400
+ private ReactiveRedisConnectionFactory getReactiveConnectionFactory () {
401
+ return (ReactiveRedisConnectionFactory ) this .connectionFactory ;
402
+ }
403
+
400
404
private static boolean shouldExpireWithin (@ Nullable Duration ttl ) {
401
405
return ttl != null && !ttl .isZero () && !ttl .isNegative ();
402
406
}
403
407
408
+ private Expiration toExpiration (Duration ttl ) {
409
+ return Expiration .from (ttl .toMillis (), TimeUnit .MILLISECONDS );
410
+ }
411
+
412
+ private Expiration toExpiration (Object key , @ Nullable Object value ) {
413
+ return Expiration .from (this .lockTtl .getTimeToLive (key , value ));
414
+ }
415
+
404
416
/**
405
417
* Interface for asynchronous cache retrieval.
406
418
*
@@ -419,8 +431,8 @@ interface AsyncCacheWriter {
419
431
* @param name the cache name from which to retrieve the cache entry.
420
432
* @param key the cache entry key.
421
433
* @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.
434
+ * @return a future that completes either with a value if the value exists or completing with {@code null}
435
+ * if the cache does not contain an entry.
424
436
*/
425
437
CompletableFuture <byte []> retrieve (String name , byte [] key , @ Nullable Duration ttl );
426
438
@@ -463,8 +475,8 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
463
475
}
464
476
465
477
/**
466
- * Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations using
467
- * {@link ReactiveRedisConnectionFactory}.
478
+ * Delegate implementing {@link AsyncCacheWriter} to provide asynchronous cache retrieval and storage operations
479
+ * using {@link ReactiveRedisConnectionFactory}.
468
480
*
469
481
* @since 3.2
470
482
*/
@@ -481,11 +493,13 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
481
493
return doWithConnection (connection -> {
482
494
483
495
ByteBuffer wrappedKey = ByteBuffer .wrap (key );
496
+
484
497
Mono <?> cacheLockCheck = isLockingCacheWriter () ? waitForLock (connection , name ) : Mono .empty ();
498
+
485
499
ReactiveStringCommands stringCommands = connection .stringCommands ();
486
500
487
501
Mono <ByteBuffer > get = shouldExpireWithin (ttl )
488
- ? stringCommands .getEx (wrappedKey , Expiration . from (ttl ))
502
+ ? stringCommands .getEx (wrappedKey , toExpiration (ttl ))
489
503
: stringCommands .get (wrappedKey );
490
504
491
505
return cacheLockCheck .then (get ).map (ByteUtils ::getBytes ).toFuture ();
@@ -498,75 +512,97 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
498
512
return doWithConnection (connection -> {
499
513
500
514
Mono <?> mono = isLockingCacheWriter ()
501
- ? doStoreWithLocking (name , key , value , ttl , connection )
515
+ ? doLockStoreUnlock (name , key , value , ttl , connection )
502
516
: doStore (key , value , ttl , connection );
503
517
504
518
return mono .then ().toFuture ();
505
519
});
506
520
}
507
521
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
-
515
522
private Mono <Boolean > doStore (byte [] cacheKey , byte [] value , @ Nullable Duration ttl ,
516
523
ReactiveRedisConnection connection ) {
517
524
518
525
ByteBuffer wrappedKey = ByteBuffer .wrap (cacheKey );
519
526
ByteBuffer wrappedValue = ByteBuffer .wrap (value );
520
527
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
- }
528
+ ReactiveStringCommands stringCommands = connection .stringCommands ();
529
+
530
+ return shouldExpireWithin (ttl )
531
+ ? stringCommands .set (wrappedKey , wrappedValue , toExpiration (ttl ), SetOption .upsert ())
532
+ : stringCommands .set (wrappedKey , wrappedValue );
527
533
}
528
534
535
+ private Mono <Boolean > doLockStoreUnlock (String name , byte [] key , byte [] value , @ Nullable Duration ttl ,
536
+ ReactiveRedisConnection connection ) {
537
+
538
+ Mono <Object > lock = doLock (name , key , value , connection );
539
+
540
+ Function <Object , Mono <Boolean >> store = unused -> doStore (key , value , ttl , connection );
541
+ Function <Object , Mono <Void >> unlock = unused -> doUnlock (name , connection );
542
+
543
+ return Mono .usingWhen (lock , store , unlock );
544
+ }
529
545
530
546
private Mono <Object > doLock (String name , Object contextualKey , @ Nullable Object contextualValue ,
531
547
ReactiveRedisConnection connection ) {
532
548
533
- ByteBuffer key = ByteBuffer . wrap ( createCacheLockKey ( name ) );
549
+ ByteBuffer key = toCacheLockKey ( name );
534
550
ByteBuffer value = ByteBuffer .wrap (new byte [0 ]);
535
- Expiration expiration = Expiration .from (lockTtl .getTimeToLive (contextualKey , contextualValue ));
551
+
552
+ Expiration expiration = toExpiration (contextualKey , contextualValue );
536
553
537
554
return connection .stringCommands ().set (key , value , expiration , SetOption .SET_IF_ABSENT ) //
538
555
// Ensure we emit an object, otherwise, the Mono.usingWhen operator doesn't run the inner resource function.
539
556
.thenReturn (Boolean .TRUE );
540
557
}
541
558
542
559
private Mono <Void > doUnlock (String name , ReactiveRedisConnection connection ) {
543
- return connection .keyCommands ().del (ByteBuffer . wrap ( createCacheLockKey ( name ) )).then ();
560
+ return connection .keyCommands ().del (toCacheLockKey ( name )).then ();
544
561
}
545
562
546
563
private Mono <Void > waitForLock (ReactiveRedisConnection connection , String cacheName ) {
547
564
548
- AtomicLong lockWaitTimeNs = new AtomicLong ();
549
- byte [] cacheLockKey = createCacheLockKey (cacheName );
565
+ AtomicLong lockWaitNanoTime = new AtomicLong ();
550
566
551
- Flux < Long > wait = Flux . interval ( Duration . ZERO , sleepTime );
552
- Mono < Boolean > exists = connection . keyCommands (). exists ( ByteBuffer . wrap ( cacheLockKey )). filter ( it -> ! it );
567
+ Consumer < org . reactivestreams . Subscription > setNanoTimeOnLockWait = subscription ->
568
+ lockWaitNanoTime . set ( System . nanoTime () );
553
569
554
- return wait .doOnSubscribe (subscription -> lockWaitTimeNs .set (System .nanoTime ())) //
555
- .flatMap (it -> exists ) //
556
- .doFinally (signalType -> statistics .incLockTime (cacheName , System .nanoTime () - lockWaitTimeNs .get ())) //
570
+ Consumer <SignalType > recordStatistics = signalType ->
571
+ statistics .incLockTime (cacheName , System .nanoTime () - lockWaitNanoTime .get ());
572
+
573
+ Function <Long , Mono <Boolean >> doWhileCacheLockExists = lockWaitTime -> connection .keyCommands ()
574
+ .exists (toCacheLockKey (cacheName )).filter (cacheLockKeyExists -> !cacheLockKeyExists );
575
+
576
+ return waitInterval (sleepTime ) //
577
+ .doOnSubscribe (setNanoTimeOnLockWait ) //
578
+ .flatMap (doWhileCacheLockExists ) //
579
+ .doFinally (recordStatistics ) //
557
580
.next () //
558
581
.then ();
559
582
}
560
583
584
+ private Flux <Long > waitInterval (Duration period ) {
585
+ return Flux .interval (Duration .ZERO , period );
586
+ }
587
+
588
+ private ByteBuffer toCacheLockKey (String cacheName ) {
589
+ return ByteBuffer .wrap (createCacheLockKey (cacheName ));
590
+ }
591
+
561
592
private <T > CompletableFuture <T > doWithConnection (
562
593
Function <ReactiveRedisConnection , CompletableFuture <T >> callback ) {
563
594
564
- ReactiveRedisConnectionFactory cf = (ReactiveRedisConnectionFactory ) connectionFactory ;
595
+ Mono <ReactiveRedisConnection > reactiveConnection =
596
+ Mono .fromSupplier (getReactiveConnectionFactory ()::getReactiveConnection );
597
+
598
+ Function <ReactiveRedisConnection , Mono <T >> commandExecution = connection ->
599
+ Mono .fromCompletionStage (callback .apply (connection ));
600
+
601
+ Function <ReactiveRedisConnection , Mono <Void >> connectionClose = ReactiveRedisConnection ::closeLater ;
602
+
603
+ Mono <T > result = Mono .usingWhen (reactiveConnection , commandExecution , connectionClose );
565
604
566
- return Mono .usingWhen (Mono .fromSupplier (cf ::getReactiveConnection ), //
567
- it -> Mono .fromCompletionStage (callback .apply (it )), //
568
- ReactiveRedisConnection ::closeLater ) //
569
- .toFuture ();
605
+ return result .toFuture ();
570
606
}
571
607
}
572
608
}
0 commit comments