文章目录
前言
在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-time
和spring.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());
}
}