前言

spring boot项目中,避免不了使用缓存当前想实现单机缓存和共享缓存的配置和切换这里使用了Caffeine实现单机缓存,Redis实现共享缓存。使用springbootstartercache只能实现全局缓存的失效时间当前想为某些缓存单独设置失效时间,自定了缓存的配置。在清除缓存时,springbootstartercache只能定向删除单个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配置为simpleredissimple时使用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 &amp;&amp; StringUtils.containsAny(keyStr, WILD_CARD)) {
            // 将key转为正则表达式
            String pattern = keyStr.replace("*", ".+")
                    .replace("?", ".");
            cache.asMap().keySet().stream().filter(k -> k instanceof String kStr &amp;&amp; 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 &amp;&amp; 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());
    }

}

原文地址:https://blog.csdn.net/u011943534/article/details/129164025

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_43646.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注