When spring-cache uses redis as the cache implementation, if the cache is deleted in bulk via @CacheEvict(allEntries = true)
, the KEYS
command of redis is used by default to match the keys to be deleted.
Example of using KEYS
Define a cache implementation class that removes all eligible caches via the @CacheEvict(allEntries = true)
annotation.
1
2
3
4
5
6
7
8
|
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Component;
@Component
public class FooCache {
@CacheEvict(cacheNames = "app::cache", allEntries = true)
public void clear () {};
}
|
Run the test method, execute the clear
method, and observe the output log.
1
2
3
4
5
6
7
|
@Autowired
private FooCache fooCache;
@Test
public void test () {
this.fooCache.clear();
}
|
1
2
3
4
5
6
7
|
io.lettuce.core.AbstractRedisClient : Connecting to Redis at localhost:6379: Success
io.lettuce.core.RedisChannelHandler : dispatching command AsyncCommand [type=KEYS, output=KeyListOutput [output=[], error='null'], commandType=io.lettuce.core.protocol.Command]
i.lettuce.core.protocol.DefaultEndpoint : [channel=0x727bd766, /127.0.0.1:49186 -> localhost/127.0.0.1:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=KEYS, output=KeyListOutput [output=[], error='null'], commandType=io.lettuce.core.protocol.Command]
io.lettuce.core.protocol.CommandHandler : [channel=0x727bd766, /127.0.0.1:49186 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] write(ctx, AsyncCommand [type=KEYS, output=KeyListOutput [output=[], error='null'], commandType=io.lettuce.core.protocol.Command], promise)
io.lettuce.core.protocol.CommandEncoder : [channel=0x727bd766, /127.0.0.1:49186 -> localhost/127.0.0.1:6379] writing command AsyncCommand [type=KEYS, output=KeyListOutput [output=[], error='null'], commandType=io.lettuce.core.protocol.Command]
io.lettuce.core.protocol.CommandHandler : [channel=0x727bd766, /127.0.0.1:49186 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Received: 4 bytes, 1 commands in the stack
io.lettuce.core.protocol.CommandHandler : [channel=0x727bd766, /127.0.0.1:49186 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Stack contains: 1 commands
|
In the DEBUG level log output from the console, you can see that the KEYS
command was used for matching.
In a production environment, because Redis is single-threaded, the performance of the KEYS
command gets slower and slower as the database data increases. Using the KEYS
command takes up a lot of processing time in the only thread, causing Redis to block and increasing the CPU usage of Redis, slowing down all requests and possibly causing the Redis server to go down. This is a very bad situation and should be disabled for production use. Imagine if Redis blocks for more than 10 seconds, which in a cluster scenario could cause the cluster to determine that Redis has failed and failover.
It is recommended to use the SCAN
command instead of the KEYS
command. The SCAN
command, which is also O(N) complex, supports wildcard lookups, does not block the main thread, and supports cursors to return data iteratively by batch, so it is a more desirable choice.
The advantage of KEYS
over SCAN
is that the keys command returns all the matched keys at once, while the scan command needs to iterate over the returned results several times to get all the matched keys, and may have the problem of returning duplicate data.
Example of using SCAN
spring-cache provides the RedisCacheManagerBuilderCustomizer
configuration class, which allows some customization of the RedisCacheManager
.
Enable SCAN
by configuring BatchStrategy
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@Configuration
public class RedisCacheScanConfiguration {
@Bean
public RedisCacheManagerBuilderCustomizer RedisCacheManagerBuilderCustomizer(RedisConnectionFactory redisConnectionFactory) {
return builder -> {
builder.cacheWriter(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory,
// A BatchStrategy using a SCAN cursors and potentially multiple DEL commands to remove allmatching keys. This strategy allows a configurable batch size to optimize for scan batching.
BatchStrategies.scan(100)));
};
}
}
|
Run the test method again and observe the output log.
1
2
3
4
5
6
7
|
io.lettuce.core.AbstractRedisClient : Connecting to Redis at localhost:6379: Success
io.lettuce.core.RedisChannelHandler : dispatching command AsyncCommand [type=SCAN, output=KeyScanOutput [output=io.lettuce.core.KeyScanCursor@34f7b44f, error='null'], commandType=io.lettuce.core.protocol.Command]
i.lettuce.core.protocol.DefaultEndpoint : [channel=0x518f4d4d, /127.0.0.1:49753 -> localhost/127.0.0.1:6379, epid=0x1] write() writeAndFlush command AsyncCommand [type=SCAN, output=KeyScanOutput [output=io.lettuce.core.KeyScanCursor@34f7b44f, error='null'], commandType=io.lettuce.core.protocol.Command]
io.lettuce.core.protocol.CommandHandler : [channel=0x518f4d4d, /127.0.0.1:49753 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] write(ctx, AsyncCommand [type=SCAN, output=KeyScanOutput [output=io.lettuce.core.KeyScanCursor@34f7b44f, error='null'], commandType=io.lettuce.core.protocol.Command], promise)
io.lettuce.core.protocol.CommandEncoder : [channel=0x518f4d4d, /127.0.0.1:49753 -> localhost/127.0.0.1:6379] writing command AsyncCommand [type=SCAN, output=KeyScanOutput [output=io.lettuce.core.KeyScanCursor@34f7b44f, error='null'], commandType=io.lettuce.core.protocol.Command]
io.lettuce.core.protocol.CommandHandler : [channel=0x518f4d4d, /127.0.0.1:49753 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Received: 15 bytes, 1 commands in the stack
io.lettuce.core.protocol.CommandHandler : [channel=0x518f4d4d, /127.0.0.1:49753 -> localhost/127.0.0.1:6379, epid=0x1, chid=0x1] Stack contains: 1 commands
|
As you can see, spring-cache is already using the SCAN
command to match the key to be deleted when deleting in bulk.