Skip to content

Commit 4d91ada

Browse files
committed
Apply non-blocking eviction when using Lettuce for RedisCache.
clear and evict methods now use asynchronous and non-blocking removal of keys when using the Lettuce Redis driver. RedisCache also supports evictIfPresent to remove cache keys immediately which is a blocking method.
1 parent e4ad967 commit 4d91ada

File tree

7 files changed

+298
-21
lines changed

7 files changed

+298
-21
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public long cleanCache(RedisConnection connection, String name, byte[] pattern)
100100
*/
101101
static class Scan implements BatchStrategy {
102102

103-
private final int batchSize;
103+
final int batchSize;
104104

105105
Scan(int batchSize) {
106106
this.batchSize = batchSize;

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

Lines changed: 134 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,20 @@
2727
import java.util.function.Consumer;
2828
import java.util.function.Function;
2929
import java.util.function.Supplier;
30+
import java.util.stream.Collectors;
3031

3132
import org.jspecify.annotations.Nullable;
33+
3234
import org.springframework.dao.PessimisticLockingFailureException;
35+
import org.springframework.data.redis.connection.ReactiveKeyCommands;
3336
import org.springframework.data.redis.connection.ReactiveRedisConnection;
3437
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
3538
import org.springframework.data.redis.connection.ReactiveStringCommands;
3639
import org.springframework.data.redis.connection.RedisConnection;
3740
import org.springframework.data.redis.connection.RedisConnectionFactory;
3841
import org.springframework.data.redis.connection.RedisStringCommands;
3942
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
43+
import org.springframework.data.redis.core.ScanOptions;
4044
import org.springframework.data.redis.core.types.Expiration;
4145
import org.springframework.data.redis.util.ByteUtils;
4246
import org.springframework.util.Assert;
@@ -311,8 +315,20 @@ public void remove(String name, byte[] key) {
311315
Assert.notNull(name, "Name must not be null");
312316
Assert.notNull(key, "Key must not be null");
313317

314-
execute(name, connection -> connection.keyCommands().del(key));
318+
if (supportsAsyncRetrieve()) {
319+
asyncCacheWriter.remove(name, key).thenRun(() -> statistics.incDeletes(name));
320+
} else {
321+
removeIfPresent(name, key);
322+
}
323+
}
324+
325+
@Override
326+
public boolean removeIfPresent(String name, byte[] key) {
327+
328+
Long removals = execute(name, connection -> connection.keyCommands().del(key));
315329
statistics.incDeletes(name);
330+
331+
return removals > 0;
316332
}
317333

318334
@Override
@@ -321,7 +337,22 @@ public void clean(String name, byte[] pattern) {
321337
Assert.notNull(name, "Name must not be null");
322338
Assert.notNull(pattern, "Pattern must not be null");
323339

324-
execute(name, connection -> {
340+
if (supportsAsyncRetrieve()) {
341+
asyncCacheWriter.clean(name, pattern, batchStrategy)
342+
.thenAccept(deleteCount -> statistics.incDeletesBy(name, deleteCount.intValue()));
343+
return;
344+
}
345+
346+
invalidate(name, pattern);
347+
}
348+
349+
@Override
350+
public boolean invalidate(String name, byte[] pattern) {
351+
352+
Assert.notNull(name, "Name must not be null");
353+
Assert.notNull(pattern, "Pattern must not be null");
354+
355+
return execute(name, connection -> {
325356

326357
try {
327358
if (isLockingCacheWriter()) {
@@ -337,13 +368,12 @@ public void clean(String name, byte[] pattern) {
337368

338369
statistics.incDeletesBy(name, (int) deleteCount);
339370

371+
return deleteCount > 0;
340372
} finally {
341373
if (isLockingCacheWriter()) {
342374
doUnlock(name, connection);
343375
}
344376
}
345-
346-
return "OK";
347377
});
348378
}
349379

@@ -499,6 +529,25 @@ interface AsyncCacheWriter {
499529
*/
500530
CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nullable Duration ttl);
501531

532+
/**
533+
* Remove a cache entry asynchronously.
534+
*
535+
* @param name the cache name which to store the cache entry to.
536+
* @param key the key for the cache entry. Must not be {@literal null}.
537+
* @return a future that signals completion.
538+
*/
539+
CompletableFuture<Void> remove(String name, byte[] key);
540+
541+
/**
542+
* Clear the cache asynchronously.
543+
*
544+
* @param name the cache name which to store the cache entry to.
545+
* @param pattern {@link String pattern} used to match Redis keys to clear.
546+
* @param batchStrategy strategy to use.
547+
* @return a future that signals completion emitting the number of removed keys.
548+
*/
549+
CompletableFuture<Long> clean(String name, byte[] pattern, BatchStrategy batchStrategy);
550+
502551
}
503552

504553
/**
@@ -524,6 +573,17 @@ public CompletableFuture<byte[]> retrieve(String name, byte[] key, @Nullable Dur
524573
public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
525574
throw new UnsupportedOperationException("async store not supported");
526575
}
576+
577+
@Override
578+
public CompletableFuture<Void> remove(String name, byte[] key) {
579+
throw new UnsupportedOperationException("async remove not supported");
580+
}
581+
582+
@Override
583+
public CompletableFuture<Long> clean(String name, byte[] pattern, BatchStrategy batchStrategy) {
584+
throw new UnsupportedOperationException("async clean not supported");
585+
}
586+
527587
}
528588

529589
/**
@@ -534,6 +594,14 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
534594
*/
535595
class AsynchronousCacheWriterDelegate implements AsyncCacheWriter {
536596

597+
private static final int DEFAULT_SCAN_BATCH_SIZE = 64;
598+
private final int cleanBatchSize;
599+
600+
public AsynchronousCacheWriterDelegate() {
601+
this.cleanBatchSize = batchStrategy instanceof BatchStrategies.Scan scan ? scan.batchSize
602+
: DEFAULT_SCAN_BATCH_SIZE;
603+
}
604+
537605
@Override
538606
public boolean isSupported() {
539607
return true;
@@ -561,20 +629,12 @@ public CompletableFuture<Void> store(String name, byte[] key, byte[] value, @Nul
561629

562630
return doWithConnection(connection -> {
563631

564-
Mono<?> mono = isLockingCacheWriter() ? doStoreWithLocking(name, key, value, ttl, connection)
565-
: doStore(key, value, ttl, connection);
632+
Mono<?> mono = doWithLocking(name, key, value, connection, () -> doStore(key, value, ttl, connection));
566633

567634
return mono.then().toFuture();
568635
});
569636
}
570637

571-
private Mono<Boolean> doStoreWithLocking(String name, byte[] key, byte[] value, @Nullable Duration ttl,
572-
ReactiveRedisConnection connection) {
573-
574-
return Mono.usingWhen(doLock(name, key, value, connection), unused -> doStore(key, value, ttl, connection),
575-
unused -> doUnlock(name, connection));
576-
}
577-
578638
@SuppressWarnings("NullAway")
579639
private Mono<Boolean> doStore(byte[] cacheKey, byte[] value, @Nullable Duration ttl,
580640
ReactiveRedisConnection connection) {
@@ -590,6 +650,65 @@ private Mono<Boolean> doStore(byte[] cacheKey, byte[] value, @Nullable Duration
590650
}
591651
}
592652

653+
@Override
654+
public CompletableFuture<Void> remove(String name, byte[] key) {
655+
656+
return doWithConnection(connection -> {
657+
658+
Mono<?> mono = doWithLocking(name, key, null, connection, () -> doRemove(key, connection));
659+
660+
return mono.then().toFuture();
661+
});
662+
}
663+
664+
@Override
665+
public CompletableFuture<Long> clean(String name, byte[] pattern, BatchStrategy batchStrategy) {
666+
667+
return doWithConnection(connection -> {
668+
669+
Mono<Long> mono = doWithLocking(name, pattern, null, connection, () -> doClean(pattern, connection));
670+
671+
return mono.toFuture();
672+
});
673+
}
674+
675+
private Mono<Long> doClean(byte[] pattern, ReactiveRedisConnection connection) {
676+
677+
ReactiveKeyCommands commands = connection.keyCommands();
678+
679+
Flux<ByteBuffer> keys;
680+
681+
if (batchStrategy instanceof BatchStrategies.Keys) {
682+
keys = commands.keys(ByteBuffer.wrap(pattern)).flatMapMany(Flux::fromIterable);
683+
} else {
684+
keys = commands.scan(ScanOptions.scanOptions().count(cleanBatchSize).match(pattern).build());
685+
}
686+
687+
return keys
688+
.buffer(cleanBatchSize) //
689+
.flatMap(commands::mUnlink) //
690+
.collect(Collectors.summingLong(Long::longValue));
691+
}
692+
693+
@SuppressWarnings("NullAway")
694+
private Mono<Long> doRemove(byte[] cacheKey, ReactiveRedisConnection connection) {
695+
696+
ByteBuffer wrappedKey = ByteBuffer.wrap(cacheKey);
697+
698+
return connection.keyCommands().unlink(wrappedKey);
699+
}
700+
701+
private <T> Mono<T> doWithLocking(String name, byte[] key, byte @Nullable [] value,
702+
ReactiveRedisConnection connection, Supplier<Mono<T>> action) {
703+
704+
if (isLockingCacheWriter()) {
705+
return Mono.usingWhen(doLock(name, key, value, connection), unused -> action.get(),
706+
unused -> doUnlock(name, connection));
707+
}
708+
709+
return action.get();
710+
}
711+
593712
private Mono<Object> doLock(String name, Object contextualKey, @Nullable Object contextualValue,
594713
ReactiveRedisConnection connection) {
595714

@@ -631,5 +750,7 @@ private <T> CompletableFuture<T> doWithConnection(
631750
ReactiveRedisConnection::closeLater) //
632751
.toFuture();
633752
}
753+
634754
}
755+
635756
}

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,12 @@ public void clear() {
235235
}
236236

237237
/**
238-
* Clear keys that match the given {@link String keyPattern}.
238+
* Clear keys that match the given {@link String keyPattern}. Useful when cache keys are formatted in a style where
239+
* Redis patterns can be used for matching these.
239240
* <p>
240-
* Useful when cache keys are formatted in a style where Redis patterns can be used for matching these.
241+
* Actual clearing may be performed in an asynchronous or deferred fashion, with subsequent lookups possibly still
242+
* seeing the entries. This may for example be the case with transactional cache decorators. Use {@link #invalidate()}
243+
* for guaranteed immediate removal of entries.
241244
*
242245
* @param keyPattern {@link String pattern} used to match Redis keys to clear.
243246
* @since 3.0
@@ -246,6 +249,11 @@ public void clear(String keyPattern) {
246249
getCacheWriter().clean(getName(), createAndConvertCacheKey(keyPattern));
247250
}
248251

252+
@Override
253+
public boolean invalidate() {
254+
return getCacheWriter().invalidate(getName(), createAndConvertCacheKey("*"));
255+
}
256+
249257
/**
250258
* Reset all statistics counters and gauges for this cache.
251259
*
@@ -260,6 +268,11 @@ public void evict(Object key) {
260268
getCacheWriter().remove(getName(), createAndConvertCacheKey(key));
261269
}
262270

271+
@Override
272+
public boolean evictIfPresent(Object key) {
273+
return getCacheWriter().removeIfPresent(getName(), createAndConvertCacheKey(key));
274+
}
275+
263276
@Override
264277
public CompletableFuture<ValueWrapper> retrieve(Object key) {
265278

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,54 @@ default CompletableFuture<byte[]> retrieve(String name, byte[] key) {
244244

245245
/**
246246
* Remove the given key from Redis.
247+
* <p>
248+
* Actual eviction may be performed in an asynchronous or deferred fashion, with subsequent lookups possibly still
249+
* seeing the entry.
247250
*
248251
* @param name cache name must not be {@literal null}.
249252
* @param key key for the cache entry. Must not be {@literal null}.
250253
*/
251254
void remove(String name, byte[] key);
252255

256+
/**
257+
* Remove the given key from Redis if it is present, expecting the key to be immediately invisible for subsequent
258+
* lookups.
259+
*
260+
* @param name cache name must not be {@literal null}.
261+
* @param key key for the cache entry. Must not be {@literal null}.
262+
* @return {@code true} if the cache was known to have a mapping for this key before, {@code false} if it did not (or
263+
* if prior presence could not be determined).
264+
*/
265+
default boolean removeIfPresent(String name, byte[] key) {
266+
remove(name, key);
267+
return false;
268+
}
269+
253270
/**
254271
* Remove all keys following the given pattern.
272+
* <p>
273+
* Actual clearing may be performed in an asynchronous or deferred fashion, with subsequent lookups possibly still
274+
* seeing the entries.
255275
*
256276
* @param name cache name must not be {@literal null}.
257277
* @param pattern pattern for the keys to remove. Must not be {@literal null}.
258278
*/
259279
void clean(String name, byte[] pattern);
260280

281+
/**
282+
* Remove all keys following the given pattern expecting all entries to be immediately invisible for subsequent
283+
* lookups.
284+
*
285+
* @param name cache name must not be {@literal null}.
286+
* @param pattern pattern for the keys to remove. Must not be {@literal null}.
287+
* @return {@code true} if the cache was known to have mappings before, {@code false} if it did not (or if prior
288+
* presence of entries could not be determined).
289+
*/
290+
default boolean invalidate(String name, byte[] pattern) {
291+
clean(name, pattern);
292+
return false;
293+
}
294+
261295
/**
262296
* Reset all statistics counters and gauges for this cache.
263297
*
@@ -323,4 +357,5 @@ static TtlFunction persistent() {
323357
Duration getTimeToLive(Object key, @Nullable Object value);
324358

325359
}
360+
326361
}

0 commit comments

Comments
 (0)