springboot学习(八十二) springboot中配置Caffeine和Redis缓存,自定义缓存失效时间,并自定义实现通配符删除缓存功能


前言

在spring boot项目中,避免不了使用缓存,当前想实现单机缓存和共享缓存的配置和切换,这里使用了Caffeine实现单机缓存,Redis实现共享缓存。使用spring-boot-starter-cache只能实现全局缓存的失效时间,当前想为某些缓存单独设置失效时间,自定了缓存的配置。在清除缓存时,spring-boot-starter-cache只能定向删除单个key,通过自定义Cache和CacheManager的形式实现通配符清除缓存的功能。


一、引入依赖

maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

gradle:

implementation "org.springframework.boot:spring-boot-starter-data-redis"
implementation "org.springframework.boot:spring-boot-starter-cache"
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.4'

二、自定义配置

1.application.properties中配置

配置如下(也可改为yaml格式):

###########################################缓存配置##################################
# 缓存的max-size配置在使用redis时会被忽略
# 使用simple类型缓存时必须执行缓存的name,否则报异常,redis可以不指定,使用默认的超时时间

# 存储token相关的缓存name
spring.cache.specs.auth.expire-time=24h
spring.cache.specs.auth.max-size=10000
# 测试缓存name
spring.cache.specs.test.expire-time=10s
spring.cache.specs.test.max-size=10
# 存储权限相关的缓存name
spring.cache.specs.permission.expire-time=10m
spring.cache.specs.permission.max-size=10000
# param name
spring.cache.specs.param.expire-time=600s
spring.cache.specs.param.max-size=10000
# dict name
spring.cache.specs.dict.expire-time=10m
spring.cache.specs.dict.max-size=10000
# 缓存验证码name
spring.cache.specs.captcha.expire-time=10m
spring.cache.specs.captcha.max-size=10000
# 存储用户锁定key的缓存name
spring.cache.specs.lockuser.expire-time=2m
spring.cache.specs.lockuser.max-size=10000

####################################simple缓存配置与redis缓存冲突####################################################
spring.cache.type=simple
####################################redis缓存配置与simple缓存冲突####################################################
#spring.cache.type=redis
# redis默认超时时间
spring.cache.redis.time-to-live=30d
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.timeout=1000000
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=-1

如上,spring.cache.specs.xxx.expire-timespring.cache.specs.xxx.max-size分别对应缓存的失效时间和最大缓存数目,xxx为自定义的缓存名,每个缓存名可配置自己的失效时间和最大缓存数目。
spring.cache.type配置为simple或redis,simple时使用Caffeine本地缓存,配置为redis时使用redis共享缓存。

2.配置对应的实体类

CacheSpec代码如下:

/**
 * 缓存配置
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/2/22 9:11
 */
@Data
@ConfigurationProperties(prefix = "spring.cache")
public class CacheSpec {
    private Map<String, Spec> specs;

    @Data
    public static class Spec {
        private Duration expireTime;

        private Integer maxSize;
    }
}

三、Caffeine缓存配置

1.自定义CaffeineCache

重写evict函数,实现使用通配符删除缓存的功能

/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/2/22 15:17
 */
public class CustomizedCaffeineCache extends CaffeineCache {
    private static final String[] WILD_CARD = new String[]{"*", "?", "[", "]"};

    private Cache<Object, Object> cache;

    public CustomizedCaffeineCache(String name, Cache<Object, Object> cache) {
        super(name, cache);
        this.cache = cache;
    }

    public CustomizedCaffeineCache(String name, Cache<Object, Object> cache, boolean allowNullValues) {
        super(name, cache, allowNullValues);
        this.cache = cache;
    }

    @Override
    public void evict(Object key) {
        if (key instanceof String keyStr && StringUtils.containsAny(keyStr, WILD_CARD)) {
            // 将key转为正则表达式
            String pattern = keyStr.replace("*", ".+")
                    .replace("?", ".");
            cache.asMap().keySet().stream().filter(k -> k instanceof String kStr && kStr.matches(pattern))
                    .forEach(super::evict);
        } else {
            super.evict(key);
        }
    }
}

2.自定义SimpleCache配置

根据配置文件的配置,为每个缓存单独设置失效时间。

/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/2/22 9:05
 */
@AutoConfiguration
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "simple")
@EnableConfigurationProperties(CacheSpec.class)
public class SimpleCacheAutoConfiguration {

    @Autowired
    private CacheSpec cacheSpec;

    @Bean
    public CacheManager cacheManager(Ticker ticker) {
        SimpleCacheManager manager = new SimpleCacheManager();
        if (cacheSpec != null) {
            List<CaffeineCache> caches = cacheSpec.getSpecs().entrySet().stream()
                            .map(entry -> buildCache(entry.getKey(), entry.getValue(), ticker))
                            .toList();
            manager.setCaches(caches);
        }
        return manager;
    }

    private CaffeineCache buildCache(String name, CacheSpec.Spec spec, Ticker ticker) {
        final Caffeine<Object, Object> caffeineBuilder = Caffeine.newBuilder()
                .expireAfterWrite(spec.getExpireTime().getSeconds(), TimeUnit.SECONDS)
                .maximumSize(spec.getMaxSize())
                .ticker(ticker);
        return new CustomizedCaffeineCache(name, caffeineBuilder.build());
    }

    @Bean
    public Ticker ticker() {
        return Ticker.systemTicker();
    }
}

如果使用的是springboot2.x的版本将@AutoConfiguration改为@Configuration

四、Redis缓存配置

1.自定义RedisCache

重写evict函数,使用scan命令通配符查找redis中的key,并删除

/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/2/22 14:17
 */
public class CustomizedRedisCache extends RedisCache {
    private static final String[] WILD_CARD = new String[]{"*", "?", "[", "]"};
    private RedisConnectionFactory connectionFactory;

    /**
     * Create new {@link RedisCache}.
     *
     * @param name        must not be {@literal null}.
     * @param cacheWriter must not be {@literal null}.
     * @param cacheConfig must not be {@literal null}.
     */
    protected CustomizedRedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig,
                                   RedisConnectionFactory connectionFactory) {
        super(name, cacheWriter, cacheConfig);
        this.connectionFactory = connectionFactory;
    }

    @Override
    public void evict(Object key) {
        if (key instanceof String keyStr && StringUtils.containsAny(keyStr, WILD_CARD)) {
            // 如果是key是字符串类型,且以*结尾,以通配符方式删除缓存
            String cacheKey = super.createCacheKey(key);
            //用scan替代keys
            RedisConnection connection = null;
            try {
                connection = connectionFactory.getConnection();
                ScanOptions scanOptions = ScanOptions.scanOptions().match(cacheKey).count(Integer.MAX_VALUE).build();
                Cursor<byte[]> cursor = connection.scan(scanOptions);
                while (cursor.hasNext()) {
                    connection.del(cursor.next());
                }
            } finally {
                if (connection != null) {
                    connection.close();
                }
            }
        } else {
            super.evict(key);
        }
    }
}

2.自定义RedisCacheManager

重写createRedisCache函数,返回自定义的CustomizedRedisCache

/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/2/22 14:16
 */
public class CustomizedRedisCacheManager extends RedisCacheManager {
    private RedisCacheWriter cacheWriter;
    private RedisCacheConfiguration defaultCacheConfiguration;
    private RedisConnectionFactory connectionFactory;
    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
    }

    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
    }

    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfiguration = defaultCacheConfiguration;
    }

    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
                                       Map<String, RedisCacheConfiguration> initialCacheConfigurations, RedisConnectionFactory connectionFactory) {
        this(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
        this.connectionFactory = connectionFactory;

    }

    public CustomizedRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
    }

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        return new CustomizedRedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfiguration, connectionFactory);
    }
}

3.自定义RedisCache配置

根据配置文件的配置,为每个缓存单独设置失效时间。



/**
 * Redis缓存配置
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2020/12/7 21:42
 * @since jdk1.8
 */
@SuppressWarnings("unused")
@AutoConfiguration
@ConditionalOnProperty(name = "spring.cache.type", havingValue = "redis")
@EnableConfigurationProperties(CacheSpec.class)
public class RedisCacheAutoConfiguration {
    @Value("${spring.cache.redis.time-to-live}")
    private Duration timeToLive;
    @Autowired
    private CacheSpec cacheSpec;

    /**
     * 序列化配置
     */
    @Bean("redisTemplate")
    @Primary
    public RedisTemplate<String, Serializable> redisTemplate (LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }


    @Bean("cacheManager")
    @Primary
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return new CustomizedRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                // 默认策略,未配置的 key 会使用这个
                this.getRedisCacheConfigurationWithTtl(timeToLive),
                // 指定 key 策略
                this.getRedisCacheConfigurationMap(),
                // redis连接工厂
                redisConnectionFactory
        );
    }

    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(8);
        cacheSpec.getSpecs().forEach((name, spec) ->
                redisCacheConfigurationMap.put(name, this.getRedisCacheConfigurationWithTtl(spec.getExpireTime())));
        return redisCacheConfigurationMap;
    }

    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Duration duration) {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializerValue = new Jackson2JsonRedisSerializer<>(om, Object.class);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        RedisSerializationContext.SerializationPair<Object> objectSerializationPair = RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializerValue);
        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(objectSerializationPair).entryTtl(duration);
        return redisCacheConfiguration;
    }

}

如果使用的是springboot2.x的版本将@AutoConfiguration改为@Configuration

五、测试

/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/2/22 9:25
 */
@RestController
@RequestMapping("/test/cache")
public class CacheControllerTest {

    /**
     * 测试缓存失效
     * */
    @GetMapping("/t1")
    @Cacheable(value = "test", key = "'aaa'")
    public ResponseEntity t1() {
        return new ResponseEntity("test" + new Date());
    }

    /**
     * 使用cacheManager测试缓存失效
     * */
    @GetMapping("/t2")
    public ResponseEntity t2() throws InterruptedException {
        CacheManager cacheManager = SpringUtils.getBean(CacheManager.class);
        Cache testCache = cacheManager.getCache("test");
        testCache.put("xxx", "yyy");
        System.out.println(testCache.get("xxx").get());
        TimeUnit.SECONDS.sleep(11);
        System.out.println(testCache.get("xxx"));
        testCache.put("xxx", "zzz");
        System.out.println(testCache.get("xxx").get());
        testCache.evict("xxx");
        System.out.println(testCache.get("xxx"));
        Cache authCache = cacheManager.getCache("auth");
        authCache.put("xxx", "mmm");
        System.out.println(authCache.get("xxx").get());
        TimeUnit.SECONDS.sleep(11);
        System.out.println(authCache.get("xxx").get());
        return new ResponseEntity();
    }

    /**
     * 测试使用不在配置文件中的缓存名称
     * */
    @GetMapping("/t3")
    public ResponseEntity t3() throws InterruptedException {
        CacheManager cacheManager = SpringUtils.getBean(CacheManager.class);
        Cache noneCache = cacheManager.getCache("none");
        System.out.println(noneCache);
        return new ResponseEntity();
    }

    /**
     * 测试使用不在配置文件中的缓存名称
     * */
    @GetMapping("/t4")
    @Cacheable(value = "none", key = "'xxx'")
    public ResponseEntity t4() throws InterruptedException {
        return new ResponseEntity("test" + new Date());
    }

    /**
     * 测试放入复杂数据类型
     * */
    @GetMapping("/t5")
    public ResponseEntity t5() throws InterruptedException {
        CacheUtils.putCache("auth", "a", new ResponseEntity<>());
        Object xxx = CacheUtils.getCache("auth", "a", Object.class);
        ResponseEntity res = CacheUtils.getCache("auth", "a", ResponseEntity.class);
//        String res2 = CacheUtils.getCache("auth", "a", String.class);

        CacheUtils.putCache("auth", new ResponseEntity<>(), new ResponseEntity<>());

        ResponseEntity res3 = CacheUtils.getCache("auth", new ResponseEntity<>("xxx"), ResponseEntity.class);
        ResponseEntity res4 = CacheUtils.getCache("auth", new ResponseEntity<>(), ResponseEntity.class);

        CacheUtils.putCache("auth", new ResponseEntity<>(), 12);
        Integer count = CacheUtils.getCache("auth", new ResponseEntity<>(), Integer.class);

        return new ResponseEntity("test" + new Date());
    }

    /**
     * 测试使用通配符删除缓存
     * */
    @GetMapping("/t6")
    public ResponseEntity t6() throws InterruptedException {
        CacheUtils.putCache("auth", "a:1", new ResponseEntity<>());
        CacheUtils.putCache("auth", "a:2", new ResponseEntity<>());
        System.out.println(CacheUtils.getCache("auth", "a:2", ResponseEntity.class));
        CacheUtils.evictCache("auth", "a:*");
        System.out.println(CacheUtils.getCache("auth", "a:2", ResponseEntity.class));
        return new ResponseEntity("test" + new Date());
    }

}

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值