Spring Cache 使用 SCAN 来批量删除缓存

在 Spring Boot 应用中使用 Spring Cache 管理缓存时,可以通过调用 @CacheEvict(allEntries=true) 注解的方法来批量删除当前缓存(cacheNames) 下的所有缓存项目。如下:

package cn.springdoc.demo.cache;

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

@Component
public class FooCahe {

    // 清除 “foo” 命名空间下的所有缓存项目
    @CacheEvict(cacheNames = "foo", allEntries = true)
    public void clear () {
    }
}

如果你的 Spring Cache 使用的缓存实现是 Redis,那么默认情况下它会使用 KEYS [pattern] 指令来获取、删除所有匹配的缓存项目。

测试方法如下:

package cn.springdoc.demo;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import cn.springdoc.demo.cache.FooCahe;

@SpringBootTest(classes = SpringdocCacheApplication.class)
class SpringdocCacheApplicationTests {

    static final Logger logger = LoggerFactory.getLogger(SpringdocCacheApplicationTests.class);

    @Autowired
    FooCahe foo;

    @Test
    void contextLoads() {
        // 批量删除缓存
        foo.clear();
    }
}

执行测试,查看输出日志(省略了前面其他无关的内容),其中 type=KEYS 表示当前执行的 Redis 命令:

... Completing command AsyncCommand [type=KEYS, output=KeyListOutput [output=[], error='null'], commandType=io.lettuce.core.protocol.Command]

不要在 Redis 中使用 KEYS 命令

Redis 中的 KEYS 命令用于获取与指定模式匹配的所有键。尽管 KEYS 命令在某些情况下很方便,但它也有一些弊端。KEYS 命令需要遍历整个 key 空间来查找匹配的 key,它可能会导致 Redis 主线程在执行期间被阻塞(它是一个阻塞命令),这会影响 Redis 的响应能力和吞吐量,还会阻塞其他 Redis 客户端的请求。在有海量 key 的 Redis 数据库中使用 KEYS 命令是一种灾难。

所以,现在很多生产环境中的 Redis 都是禁止了 KEYS 命令的。

为了避免 KEYS 命令的弊端,推荐使用更适合的 SCAN 命令,它可以更高效地处理大型数据集,并提供更灵活的查询和操作选项。

配置 Spring Cache,使用 SCAN 来批量删除数据

Spring Cache 提供了 RedisCacheManagerBuilderCustomizer 接口,允许我们以编程的方式对 RedisCacheManager 进行一些自定义。

通过这个接口,我们可以配置 Spring Cache,以使用 SCAN 来批量删除数据,如下:

package cn.springdoc.demo.configuration;

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 CacheConfiguration{

    @Bean
    public RedisCacheManagerBuilderCustomizer RedisCacheManagerBuilderCustomizer(RedisConnectionFactory redisConnectionFactory) {
        return builder -> {
            builder.cacheWriter(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory,
                    // BatchStrategy 使用 SCAN 游标和多个 DEL 命令来删除所有匹配的键。
                    // 配置批量大小,以优化扫描批处理。
                    BatchStrategies.scan(100))); 
        };
    }
}

配置完毕后,再次执行上述测试代码,查看控制台日志输出(省略了前面无关的日志内容):

... Completing command AsyncCommand [type=SCAN, output=KeyScanOutput [output=io.lettuce.core.KeyScanCursor@174cb0d8, error='null'], commandType=io.lettuce.core.protocol.Command]

如你所见,日志中的 type=SCAN 表示已经是使用 SCAN 命令来批量删除缓存项目了,配置成功。