diff --git a/src/main/asciidoc/reference/redis-cache.adoc b/src/main/asciidoc/reference/redis-cache.adoc index bd2c5d22f..c06c975e8 100644 --- a/src/main/asciidoc/reference/redis-cache.adoc +++ b/src/main/asciidoc/reference/redis-cache.adoc @@ -26,7 +26,7 @@ RedisCacheManager cm = RedisCacheManager.builder(connectionFactory) As shown in the preceding example, `RedisCacheManager` allows definition of configurations on a per-cache basis. -The behavior of `RedisCache` created with `RedisCacheManager` is defined with `RedisCacheConfiguration`. The configuration lets you set key expiration times, prefixes, and ``RedisSerializer`` implementations for converting to and from the binary storage format, as shown in the following example: +The behavior of `RedisCache` created with `RedisCacheManager` is defined with `RedisCacheConfiguration`. The configuration lets you set key expiration times, prefixes, and `RedisSerializer` implementations for converting to and from the binary storage format, as shown in the following example: [source,java] ---- @@ -70,7 +70,7 @@ The cache implementation defaults to use `KEYS` and `DEL` to clear the cache. `K [source,java] ---- -RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategy.scan(1000))) +RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000))) .cacheDefaults(defaultCacheConfig()) ... ---- diff --git a/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java b/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java new file mode 100644 index 000000000..eb33dc51d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/cache/BatchStrategies.java @@ -0,0 +1,162 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.cache; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.util.Assert; + +/** + * A collection of predefined {@link BatchStrategy} implementations using {@code KEYS} or {@code SCAN} command. + * + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.6 + */ +public abstract class BatchStrategies { + + private BatchStrategies() { + // can't touch this - oh-oh oh oh oh-oh-oh + } + + /** + * A {@link BatchStrategy} using a single {@code KEYS} and {@code DEL} command to remove all matching keys. + * {@code KEYS} scans the entire keyspace of the Redis database and can block the Redis worker thread for a long time + * on large keyspaces. + *

+ * {@code KEYS} is supported for standalone and clustered (sharded) Redis operation modes. + * + * @return batching strategy using {@code KEYS}. + */ + public static BatchStrategy keys() { + return Keys.INSTANCE; + } + + /** + * A {@link BatchStrategy} using a {@code SCAN} cursors and potentially multiple {@code DEL} commands to remove all + * matching keys. This strategy allows a configurable batch size to optimize for scan batching. + *

+ * Note that using the {@code SCAN} strategy might be not supported on all drivers and Redis operation modes. + * + * @return batching strategy using {@code SCAN}. + */ + public static BatchStrategy scan(int batchSize) { + + Assert.isTrue(batchSize > 0, "Batch size must be greater than zero!"); + + return new Scan(batchSize); + } + + /** + * {@link BatchStrategy} using {@code KEYS}. + */ + static class Keys implements BatchStrategy { + + static Keys INSTANCE = new Keys(); + + @Override + public long cleanCache(RedisConnection connection, String name, byte[] pattern) { + + byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet()) + .toArray(new byte[0][]); + + if (keys.length > 0) { + connection.del(keys); + } + + return keys.length; + } + } + + /** + * {@link BatchStrategy} using {@code SCAN}. + */ + static class Scan implements BatchStrategy { + + private final int batchSize; + + Scan(int batchSize) { + this.batchSize = batchSize; + } + + @Override + public long cleanCache(RedisConnection connection, String name, byte[] pattern) { + + Cursor cursor = connection.scan(ScanOptions.scanOptions().count(batchSize).match(pattern).build()); + + long count = 0; + + PartitionIterator partitions = new PartitionIterator<>(cursor, batchSize); + while (partitions.hasNext()) { + + List keys = partitions.next(); + count += keys.size(); + + if (keys.size() > 0) { + connection.del(keys.toArray(new byte[0][])); + } + } + + return count; + } + } + + /** + * Utility to split and buffer outcome from a {@link Iterator} into {@link List lists} of {@code T} with a maximum + * chunks {@code size}. + * + * @param + */ + static class PartitionIterator implements Iterator> { + + private final Iterator iterator; + private final int size; + + PartitionIterator(Iterator iterator, int size) { + + this.iterator = iterator; + this.size = size; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public List next() { + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + List list = new ArrayList<>(size); + while (list.size() < size && iterator.hasNext()) { + list.add(iterator.next()); + } + + return list; + } + } +} diff --git a/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java b/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java index 9c41f6c80..bb9fd46d1 100644 --- a/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java +++ b/src/main/java/org/springframework/data/redis/cache/BatchStrategy.java @@ -15,156 +15,30 @@ */ package org.springframework.data.redis.cache; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; - import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.core.Cursor; -import org.springframework.data.redis.core.ScanOptions; -import org.springframework.util.Assert; /** - * Batch strategies to be used with {@link RedisCacheWriter}. + * A {@link BatchStrategy} to be used with {@link RedisCacheWriter}. *

- * Primarily used to clear the cache. + * Mainly used to clear the cache. + *

+ * Predefined strategies using the {@link BatchStrategies#keys() KEYS} or {@link BatchStrategies#scan(int) SCAN} + * commands can be found in {@link BatchStrategies}. * * @author Mark Paluch + * @author Christoph Strobl * @since 2.6 */ -public abstract class BatchStrategy { - - /** - * Batching strategy using a single {@code KEYS} and {@code DEL} command to remove all matching keys. {@code KEYS} - * scans the entire keyspace of the Redis database and can block the Redis worker thread for a long time on large - * keyspaces. - *

- * {@code KEYS} is supported for standalone and clustered (sharded) Redis operation modes. - * - * @return batching strategy using {@code KEYS}. - */ - public static BatchStrategy keys() { - return Keys.INSTANCE; - } - - /** - * Batching strategy using a {@code SCAN} cursors and potentially multiple {@code DEL} commands to remove all matching - * keys. This strategy allows a configurable batch size to optimize for scan batching. - *

- * Note that using the {@code SCAN} strategy might be not supported on all drivers and Redis operation modes. - * - * @return batching strategy using {@code SCAN}. - */ - public static BatchStrategy scan(int batchSize) { - - Assert.isTrue(batchSize > 0, "Batch size must be greater than zero"); - - return new Scan(batchSize); - } +public interface BatchStrategy { /** * Remove all keys following the given pattern. * - * @param the connection to use. - * @param name The cache name must not be {@literal null}. + * @param connection the connection to use. Must not be {@literal null}. + * @param name The cache name. Must not be {@literal null}. * @param pattern The pattern for the keys to remove. Must not be {@literal null}. * @return number of removed keys. */ - abstract int cleanCache(RedisConnection connection, String name, byte[] pattern); - - /** - * {@link BatchStrategy} using {@code KEYS}. - */ - static class Keys extends BatchStrategy { - - static Keys INSTANCE = new Keys(); - - @Override - int cleanCache(RedisConnection connection, String name, byte[] pattern) { - - byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet()) - .toArray(new byte[0][]); - - if (keys.length > 0) { - connection.del(keys); - } - - return keys.length; - } - } - - /** - * {@link BatchStrategy} using {@code SCAN}. - */ - static class Scan extends BatchStrategy { - - private final int batchSize; - - public Scan(int batchSize) { - this.batchSize = batchSize; - } - - @Override - int cleanCache(RedisConnection connection, String name, byte[] pattern) { - - Cursor cursor = connection.scan(ScanOptions.scanOptions().count(batchSize).match(pattern).build()); - - PartitionIterator partitions = new PartitionIterator<>(cursor, batchSize); - - int count = 0; - - while (partitions.hasNext()) { - - List keys = partitions.next(); - count += keys.size(); - - if (keys.size() > 0) { - connection.del(keys.toArray(new byte[0][])); - } - } - - return count; - } - } - - /** - * Utility to split and buffer outcome from a {@link Iterator} into {@link List lists} of {@code T} with a maximum - * chunks {@code size}. - * - * @param - */ - static class PartitionIterator implements Iterator> { - - private final Iterator iterator; - private final int size; - - public PartitionIterator(Iterator iterator, int size) { - this.iterator = iterator; - this.size = size; - } - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public List next() { - - if (!hasNext()) { - throw new NoSuchElementException(); - } - - List list = new ArrayList<>(size); - while (list.size() < size && iterator.hasNext()) { - list.add(iterator.next()); - } - - return list; - } - } + long cleanCache(RedisConnection connection, String name, byte[] pattern); } diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java index 6cfe7b950..8afa0f8d7 100644 --- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java @@ -217,8 +217,12 @@ class DefaultRedisCacheWriter implements RedisCacheWriter { wasLocked = true; } - - statistics.incDeletesBy(name, batchStrategy.cleanCache(connection, name, pattern)); + long deleteCount = batchStrategy.cleanCache(connection, name, pattern); + while (deleteCount > Integer.MAX_VALUE) { + statistics.incDeletesBy(name, Integer.MAX_VALUE); + deleteCount -= Integer.MAX_VALUE; + } + statistics.incDeletesBy(name, (int) deleteCount); } finally { diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java index abe121ee8..0210c8912 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java @@ -43,7 +43,7 @@ public interface RedisCacheWriter extends CacheStatisticsProvider { * @return new instance of {@link DefaultRedisCacheWriter}. */ static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { - return nonLockingRedisCacheWriter(connectionFactory, BatchStrategy.keys()); + return nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.keys()); } /** @@ -70,7 +70,7 @@ public interface RedisCacheWriter extends CacheStatisticsProvider { * @return new instance of {@link DefaultRedisCacheWriter}. */ static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) { - return lockingRedisCacheWriter(connectionFactory, BatchStrategy.keys()); + return lockingRedisCacheWriter(connectionFactory, BatchStrategies.keys()); } /** diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java index fa3536f28..f66bab744 100644 --- a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java +++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java @@ -307,7 +307,7 @@ public class DefaultRedisCacheWriterTests { Thread th = new Thread(() -> { DefaultRedisCacheWriter writer = new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50), - BatchStrategy.keys()) { + BatchStrategies.keys()) { @Override boolean doCheckLock(String name, RedisConnection connection) { diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java index a32d7c4bf..ef29b1378 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java @@ -262,7 +262,7 @@ public class RedisCacheTests { } RedisCache cache = new RedisCache("cache", - RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategy.scan(25)), + RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(25)), RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.fromSerializer(serializer))); doWithConnection(connection -> {