Skip to content

Commit 8c05f19

Browse files
Transition BatchStrategy to interface.
...and use long return type instead of int for cleanCache. Original Pull Request: #2051
1 parent 1fcd9af commit 8c05f19

File tree

7 files changed

+184
-144
lines changed

7 files changed

+184
-144
lines changed

src/main/asciidoc/reference/redis-cache.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ RedisCacheManager cm = RedisCacheManager.builder(connectionFactory)
2626

2727
As shown in the preceding example, `RedisCacheManager` allows definition of configurations on a per-cache basis.
2828

29-
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:
29+
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:
3030

3131
[source,java]
3232
----
@@ -70,7 +70,7 @@ The cache implementation defaults to use `KEYS` and `DEL` to clear the cache. `K
7070

7171
[source,java]
7272
----
73-
RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategy.scan(1000)))
73+
RedisCacheManager cm = RedisCacheManager.build(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(1000)))
7474
.cacheDefaults(defaultCacheConfig())
7575
...
7676
----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.cache;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.Iterator;
21+
import java.util.List;
22+
import java.util.NoSuchElementException;
23+
import java.util.Optional;
24+
25+
import org.springframework.data.redis.connection.RedisConnection;
26+
import org.springframework.data.redis.core.Cursor;
27+
import org.springframework.data.redis.core.ScanOptions;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* A collection of predefined {@link BatchStrategy} implementations using {@code KEYS} or {@code SCAN} command.
32+
*
33+
* @author Mark Paluch
34+
* @author Christoph Strobl
35+
* @since 2.6
36+
*/
37+
public abstract class BatchStrategies {
38+
39+
private BatchStrategies() {
40+
// can't touch this - oh-oh oh oh oh-oh-oh
41+
}
42+
43+
/**
44+
* A {@link BatchStrategy} using a single {@code KEYS} and {@code DEL} command to remove all matching keys.
45+
* {@code KEYS} scans the entire keyspace of the Redis database and can block the Redis worker thread for a long time
46+
* on large keyspaces.
47+
* <p/>
48+
* {@code KEYS} is supported for standalone and clustered (sharded) Redis operation modes.
49+
*
50+
* @return batching strategy using {@code KEYS}.
51+
*/
52+
public static BatchStrategy keys() {
53+
return Keys.INSTANCE;
54+
}
55+
56+
/**
57+
* A {@link BatchStrategy} using a {@code SCAN} cursors and potentially multiple {@code DEL} commands to remove all
58+
* matching keys. This strategy allows a configurable batch size to optimize for scan batching.
59+
* <p/>
60+
* Note that using the {@code SCAN} strategy might be not supported on all drivers and Redis operation modes.
61+
*
62+
* @return batching strategy using {@code SCAN}.
63+
*/
64+
public static BatchStrategy scan(int batchSize) {
65+
66+
Assert.isTrue(batchSize > 0, "Batch size must be greater than zero!");
67+
68+
return new Scan(batchSize);
69+
}
70+
71+
/**
72+
* {@link BatchStrategy} using {@code KEYS}.
73+
*/
74+
static class Keys implements BatchStrategy {
75+
76+
static Keys INSTANCE = new Keys();
77+
78+
@Override
79+
public long cleanCache(RedisConnection connection, String name, byte[] pattern) {
80+
81+
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
82+
.toArray(new byte[0][]);
83+
84+
if (keys.length > 0) {
85+
connection.del(keys);
86+
}
87+
88+
return keys.length;
89+
}
90+
}
91+
92+
/**
93+
* {@link BatchStrategy} using {@code SCAN}.
94+
*/
95+
static class Scan implements BatchStrategy {
96+
97+
private final int batchSize;
98+
99+
Scan(int batchSize) {
100+
this.batchSize = batchSize;
101+
}
102+
103+
@Override
104+
public long cleanCache(RedisConnection connection, String name, byte[] pattern) {
105+
106+
Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(batchSize).match(pattern).build());
107+
108+
long count = 0;
109+
110+
PartitionIterator<byte[]> partitions = new PartitionIterator<>(cursor, batchSize);
111+
while (partitions.hasNext()) {
112+
113+
List<byte[]> keys = partitions.next();
114+
count += keys.size();
115+
116+
if (keys.size() > 0) {
117+
connection.del(keys.toArray(new byte[0][]));
118+
}
119+
}
120+
121+
return count;
122+
}
123+
}
124+
125+
/**
126+
* Utility to split and buffer outcome from a {@link Iterator} into {@link List lists} of {@code T} with a maximum
127+
* chunks {@code size}.
128+
*
129+
* @param <T>
130+
*/
131+
static class PartitionIterator<T> implements Iterator<List<T>> {
132+
133+
private final Iterator<T> iterator;
134+
private final int size;
135+
136+
PartitionIterator(Iterator<T> iterator, int size) {
137+
138+
this.iterator = iterator;
139+
this.size = size;
140+
}
141+
142+
@Override
143+
public boolean hasNext() {
144+
return iterator.hasNext();
145+
}
146+
147+
@Override
148+
public List<T> next() {
149+
150+
if (!hasNext()) {
151+
throw new NoSuchElementException();
152+
}
153+
154+
List<T> list = new ArrayList<>(size);
155+
while (list.size() < size && iterator.hasNext()) {
156+
list.add(iterator.next());
157+
}
158+
159+
return list;
160+
}
161+
}
162+
}

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

+10-136
Original file line numberDiff line numberDiff line change
@@ -15,156 +15,30 @@
1515
*/
1616
package org.springframework.data.redis.cache;
1717

18-
import java.util.ArrayList;
19-
import java.util.Collections;
20-
import java.util.Iterator;
21-
import java.util.List;
22-
import java.util.NoSuchElementException;
23-
import java.util.Optional;
24-
2518
import org.springframework.data.redis.connection.RedisConnection;
26-
import org.springframework.data.redis.core.Cursor;
27-
import org.springframework.data.redis.core.ScanOptions;
28-
import org.springframework.util.Assert;
2919

3020
/**
31-
* Batch strategies to be used with {@link RedisCacheWriter}.
21+
* A {@link BatchStrategy} to be used with {@link RedisCacheWriter}.
22+
* <p/>
23+
* Mainly used to clear the cache.
3224
* <p/>
33-
* Primarily used to clear the cache.
25+
* Predefined strategies using the {@link BatchStrategies#keys() KEYS} or {@link BatchStrategies#scan(int) SCAN}
26+
* commands can be found in {@link BatchStrategies}.
3427
*
3528
* @author Mark Paluch
29+
* @author Christoph Strobl
3630
* @since 2.6
3731
*/
38-
public abstract class BatchStrategy {
39-
40-
/**
41-
* Batching strategy using a single {@code KEYS} and {@code DEL} command to remove all matching keys. {@code KEYS}
42-
* scans the entire keyspace of the Redis database and can block the Redis worker thread for a long time on large
43-
* keyspaces.
44-
* <p/>
45-
* {@code KEYS} is supported for standalone and clustered (sharded) Redis operation modes.
46-
*
47-
* @return batching strategy using {@code KEYS}.
48-
*/
49-
public static BatchStrategy keys() {
50-
return Keys.INSTANCE;
51-
}
52-
53-
/**
54-
* Batching strategy using a {@code SCAN} cursors and potentially multiple {@code DEL} commands to remove all matching
55-
* keys. This strategy allows a configurable batch size to optimize for scan batching.
56-
* <p/>
57-
* Note that using the {@code SCAN} strategy might be not supported on all drivers and Redis operation modes.
58-
*
59-
* @return batching strategy using {@code SCAN}.
60-
*/
61-
public static BatchStrategy scan(int batchSize) {
62-
63-
Assert.isTrue(batchSize > 0, "Batch size must be greater than zero");
64-
65-
return new Scan(batchSize);
66-
}
32+
public interface BatchStrategy {
6733

6834
/**
6935
* Remove all keys following the given pattern.
7036
*
71-
* @param the connection to use.
72-
* @param name The cache name must not be {@literal null}.
37+
* @param connection the connection to use. Must not be {@literal null}.
38+
* @param name The cache name. Must not be {@literal null}.
7339
* @param pattern The pattern for the keys to remove. Must not be {@literal null}.
7440
* @return number of removed keys.
7541
*/
76-
abstract int cleanCache(RedisConnection connection, String name, byte[] pattern);
77-
78-
/**
79-
* {@link BatchStrategy} using {@code KEYS}.
80-
*/
81-
static class Keys extends BatchStrategy {
82-
83-
static Keys INSTANCE = new Keys();
84-
85-
@Override
86-
int cleanCache(RedisConnection connection, String name, byte[] pattern) {
87-
88-
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
89-
.toArray(new byte[0][]);
90-
91-
if (keys.length > 0) {
92-
connection.del(keys);
93-
}
94-
95-
return keys.length;
96-
}
97-
}
98-
99-
/**
100-
* {@link BatchStrategy} using {@code SCAN}.
101-
*/
102-
static class Scan extends BatchStrategy {
103-
104-
private final int batchSize;
105-
106-
public Scan(int batchSize) {
107-
this.batchSize = batchSize;
108-
}
109-
110-
@Override
111-
int cleanCache(RedisConnection connection, String name, byte[] pattern) {
112-
113-
Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(batchSize).match(pattern).build());
114-
115-
PartitionIterator<byte[]> partitions = new PartitionIterator<>(cursor, batchSize);
116-
117-
int count = 0;
118-
119-
while (partitions.hasNext()) {
120-
121-
List<byte[]> keys = partitions.next();
122-
count += keys.size();
123-
124-
if (keys.size() > 0) {
125-
connection.del(keys.toArray(new byte[0][]));
126-
}
127-
}
128-
129-
return count;
130-
}
131-
}
132-
133-
/**
134-
* Utility to split and buffer outcome from a {@link Iterator} into {@link List lists} of {@code T} with a maximum
135-
* chunks {@code size}.
136-
*
137-
* @param <T>
138-
*/
139-
static class PartitionIterator<T> implements Iterator<List<T>> {
140-
141-
private final Iterator<T> iterator;
142-
private final int size;
143-
144-
public PartitionIterator(Iterator<T> iterator, int size) {
145-
this.iterator = iterator;
146-
this.size = size;
147-
}
148-
149-
@Override
150-
public boolean hasNext() {
151-
return iterator.hasNext();
152-
}
153-
154-
@Override
155-
public List<T> next() {
156-
157-
if (!hasNext()) {
158-
throw new NoSuchElementException();
159-
}
160-
161-
List<T> list = new ArrayList<>(size);
162-
while (list.size() < size && iterator.hasNext()) {
163-
list.add(iterator.next());
164-
}
165-
166-
return list;
167-
}
168-
}
42+
long cleanCache(RedisConnection connection, String name, byte[] pattern);
16943

17044
}

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,12 @@ public void clean(String name, byte[] pattern) {
217217
wasLocked = true;
218218
}
219219

220-
221-
statistics.incDeletesBy(name, batchStrategy.cleanCache(connection, name, pattern));
220+
long deleteCount = batchStrategy.cleanCache(connection, name, pattern);
221+
while (deleteCount > Integer.MAX_VALUE) {
222+
statistics.incDeletesBy(name, Integer.MAX_VALUE);
223+
deleteCount -= Integer.MAX_VALUE;
224+
}
225+
statistics.incDeletesBy(name, (int) deleteCount);
222226

223227
} finally {
224228

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
4343
* @return new instance of {@link DefaultRedisCacheWriter}.
4444
*/
4545
static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
46-
return nonLockingRedisCacheWriter(connectionFactory, BatchStrategy.keys());
46+
return nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.keys());
4747
}
4848

4949
/**
@@ -70,7 +70,7 @@ static RedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connec
7070
* @return new instance of {@link DefaultRedisCacheWriter}.
7171
*/
7272
static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
73-
return lockingRedisCacheWriter(connectionFactory, BatchStrategy.keys());
73+
return lockingRedisCacheWriter(connectionFactory, BatchStrategies.keys());
7474
}
7575

7676
/**

src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ void lockingCacheWriterShouldExitWhenInterruptedWaitForLockRelease() throws Inte
307307
Thread th = new Thread(() -> {
308308

309309
DefaultRedisCacheWriter writer = new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50),
310-
BatchStrategy.keys()) {
310+
BatchStrategies.keys()) {
311311

312312
@Override
313313
boolean doCheckLock(String name, RedisConnection connection) {

src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ void clearWithScanShouldClearCache() {
262262
}
263263

264264
RedisCache cache = new RedisCache("cache",
265-
RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategy.scan(25)),
265+
RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, BatchStrategies.scan(25)),
266266
RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.fromSerializer(serializer)));
267267

268268
doWithConnection(connection -> {

0 commit comments

Comments
 (0)