Spring Cache Redis 使用 Scan 来匹配要批量驱逐的key

spring-cache 使用 redis 作为缓存实现时,如果通过 @CacheEvict(allEntries = true) 批量删除缓存,默认会使用redis的 keys 命令来匹配需要删除的key。

定义一个缓存实现类,通过 @CacheEvict(allEntries = true) 注解来删除所有符合条件的缓存。

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Component;

@Component
public class FooCache {
	
	@CacheEvict(cacheNames = "app::cache", allEntries = true)
	public void clear () {};
}

运行测试方法,观察输出日志。

@Autowired
private FooCache fooCache;

@Test
public void test () {
	this.fooCache.clear();
}

通过输出的日志,可以看到使用了 keys 命令进行匹配。

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

在生产环境中执行 KEYS 命令的时,因为Redis是单线程的,KEYS 命令的性能随着数据库数据的增多而越来越慢,使用 KEYS 命令时会占用唯一的一个线程的大量处理时间,引发Redis阻塞并且增加Redis的CPU占用,导致所有的请求都被拖慢,可能造成Redis所在的服务器宕机。情况是很恶劣的,在实际生产运用的过程中应该禁用这个命令。试想如果Redis阻塞超过10秒,如果是在集群的场景下,可能导致集群判断Redis已经故障,从而进行故障切换。

修改key的匹配命令为 scan

spring-cache 提供了 RedisCacheManagerBuilderCustomizer 配置类,可以自定义 redis cache 的一些行为。

通过配置 BatchStrategy 启用 scan

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,
					BatchStrategies.scan(100)));
		};
	}
}

添加配置后,再次运行测试方法,观察输出日志。

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

可以看到,spring-cache 在批量删除时,是使用 scan 来匹配要删除的key。