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.