本文参考了spring boot + spring cache 实现两级缓存(redis + caffeine)。
与spring boot + spring cache 实现两级缓存(redis + caffeine)一致:
空值的包装
和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类1.@EnableCaching
:启用spring cache缓存,在spring boot的启动类或配置类上需要加上此注解才会生效
2.yml
# redis-starter的配置
spring:
cache:
cache-names: cache1,cache2,cache3
redis:
timeout: 10000
pool:
max-idle: 10
min-idle: 2
max-active: 10
max-wait: 3000
#自定义配置。expire统一单位为毫秒
cache:
multi:
cacheNames: cache1,cache2,cache3
ehcache:
expireAfterWrite: 5000
maxEntry: 1000
redis:
defaultExpiration: 60000
expires:
cache1: 50000
cache2: 70000
cache3: 70000
3.POM
依赖项
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-redisartifactId>
<version>1.4.3.RELEASEversion>
dependency>
<dependency>
<groupId>org.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>3.5.2version>
dependency>
定义properties配置属性类
@ConfigurationProperties(prefix = "cache.multi")
@Data
public class RedisEhcacheProperties {
private Set cacheNames = new HashSet<>();
/** 是否存储空值,默认true,防止缓存穿透*/
private boolean cacheNullValues = true;
/** 是否动态根据cacheName创建Cache的实现,默认true*/
private boolean dynamic = true;
/** 缓存key的前缀*/
private String cachePrefix;
private Redis redis = new Redis();
private Ehcache ehcache = new Ehcache();
public boolean isCacheNullValues() {
return cacheNullValues;
}
@Data
public class Redis {
/** 全局过期时间,单位毫秒,默认不过期*/
private long defaultExpiration = 0;
/** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高*/
private Map expires = new HashMap<>();
/** 缓存更新时通知其他节点的topic名称*/
private String topic = "cache:redis:ehcache:topic";
}
@Data
public class Ehcache {
/**
* 访问后过期时间,单位毫秒
*/
// private long expireAfterAccess;
/**
* 写入后过期时间,单位毫秒
*/
private long expireAfterWrite;
/**
* 初始化大小
*/
// private int initialCapacity;
/**
* 每个ehcache最大缓存对象个数,超过此数量时按照失效策略(默认为LRU)
*/
private long maxEntry = 500;
}
}
RedisEhcacheCache
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Slf4j
public class RedisEhcacheCache extends AbstractValueAdaptingCache {
private String name;
private RedisTemplate
实现CacheManager接口
import lombok.extern.slf4j.Slf4j;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.*;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Slf4j
public class RedisEhcacheCacheManager implements CacheManager {
private ConcurrentMap cacheMap = new ConcurrentHashMap();
private RedisEhcacheProperties redisEhcacheProperties;
private RedisTemplate redisTemplate;
private boolean dynamic = true;
private Set cacheNames;
private org.ehcache.CacheManager ehCacheManager;
private CacheConfiguration configuration;
public RedisEhcacheCacheManager(RedisEhcacheProperties redisEhcacheProperties,
RedisTemplate redisTemplate) {
super();
this.redisEhcacheProperties = redisEhcacheProperties;
this.redisTemplate = redisTemplate;
this.dynamic = redisEhcacheProperties.isDynamic();
this.cacheNames = redisEhcacheProperties.getCacheNames();
setAboutEhCache();
}
private void setAboutEhCache(){
long ehcacheExpire = redisEhcacheProperties.getEhcache().getExpireAfterWrite();
this.configuration =
CacheConfigurationBuilder
.newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(redisEhcacheProperties.getEhcache().getMaxEntry()))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcacheExpire)))
.build();
this.ehCacheManager = CacheManagerBuilder
.newCacheManagerBuilder()
.build();
this.ehCacheManager.init();
}
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if(cache != null) {
return cache;
}
if(!dynamic && !cacheNames.contains(name)) {
return cache;
}
cache = new RedisEhcacheCache(name, redisTemplate, getEhcache(name), redisEhcacheProperties);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
log.debug("create cache instance, the cache name is : {}", name);
return oldCache == null ? cache : oldCache;
}
public org.ehcache.Cache getEhcache(String name){
org.ehcache.Cache res = ehCacheManager.getCache(name, Object.class, Object.class);
if(res != null){
return res;
}
return ehCacheManager.createCache(name, configuration);
}
@Override
public Collection getCacheNames() {
return this.cacheNames;
}
public void clearLocal(String cacheName, Object key, Integer sender) {
Cache cache = cacheMap.get(cacheName);
if(cache == null) {
return ;
}
RedisEhcacheCache redisEhcacheCache = (RedisEhcacheCache) cache;
//如果是发送者本身发送的消息,就不进行key的清除
if(redisEhcacheCache.getLocalCache().hashCode() != sender) {
redisEhcacheCache.clearLocal(key);
}
}
}
redis消息发布/订阅,传输的消息类
@Data
public class CacheMessage implements Serializable {
private static final long serialVersionUID = 5987219310442078193L;
private String cacheName;
private Object key;
private Integer sender;
public CacheMessage(String cacheName, Object key) {
super();
this.cacheName = cacheName;
this.key = key;
}
public CacheMessage(String cacheName, Object key, Integer sender) {
super();
this.cacheName = cacheName;
this.key = key;
this.sender = sender;
}
}
监听redis消息需要实现MessageListener接口
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
/**
* 监听redis消息需要实现MessageListener接口
*/
@Slf4j
public class CacheMessageListener implements MessageListener {
private RedisTemplate redisTemplate;
private RedisEhcacheCacheManager redisEhcacheCacheManager;
public CacheMessageListener(RedisTemplate redisTemplate,
RedisEhcacheCacheManager redisEhcacheCacheManager) {
super();
this.redisTemplate = redisTemplate;
this.redisEhcacheCacheManager = redisEhcacheCacheManager;
}
@Override
public void onMessage(Message message, byte[] pattern) {
CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
log.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
redisEhcacheCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey(), cacheMessage.getSender());
}
}
增加spring boot配置类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(RedisEhcacheProperties.class)
public class CacheRedisEhcacheAutoConfiguration {
@Autowired
private RedisEhcacheProperties redisEhcacheProperties;
@Bean
public RedisEhcacheCacheManager cacheManager(RedisTemplate redisTemplate) {
return new RedisEhcacheCacheManager(redisEhcacheProperties, redisTemplate);
}
@Bean
@ConditionalOnBean(RedisEhcacheCacheManager.class)
public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate redisTemplate,
RedisEhcacheCacheManager redisEhcacheCacheManager) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisEhcacheCacheManager);
redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(redisEhcacheProperties.getRedis().getTopic()));
return redisMessageListenerContainer;
}
}
缓存使用
//cacheManager = "cacheManager"可以不指定
@Cacheable(value = "gerritCache", key = "#projectName + '_' + #from + '_' + #to"/*, cacheManager = "cacheManager"*/)
public UserVO get(long id) {
logger.info("get by id from db");
UserVO user = new UserVO();
user.setId(id);
user.setName("name" + id);
user.setCreateTime(TimestampUtil.current());
return user;
}
RedisCacheConfiguration
和我们自定义的CacheRedisEhcacheAutoConfiguration
都有注解:
@AutoConfigureAfter(RedisAutoConfiguration.class)
不过由于RedisCacheConfiguration
有:
@ConditionalOnMissingBean(CacheManager.class)
保证了唯一性:如果CacheRedisEhcacheAutoConfiguration
被执行了,那么RedisCacheConfiguration
就不会被执行。
我们可以基于这一点做一个二级缓存开关。在yml加入
cache:
use2L: true #开启二级缓存
CacheRedisEhcacheAutoConfiguration加上(yml没有配置或者配置为false,二级缓存都不起作用):
@ConditionalOnProperty(name = "cache.use2L", havingValue = "true", matchIfMissing = false)
加上CacheConfig
对单独一级redis缓存进行配置:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConditionalOnProperty(name = "cache.use2L", havingValue = "false", matchIfMissing = true)
@EnableConfigurationProperties(RedisEhcacheProperties.class)
public class CacheConfig {
@Autowired
private RedisEhcacheProperties redisEhcacheProperties;
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
//设置各个cache的缓存过期时间
Map expires = new HashMap<>(redisEhcacheProperties.getRedis().getExpires());
//毫秒->秒
expires.forEach((k, v) -> expires.put(k, v/1000));
rcm.setExpires(expires);
rcm.setDefaultExpiration(redisEhcacheProperties.getRedis().getDefaultExpiration());//默认过期时间
return rcm;
}
}
ref: 配置Spring Boot通过@ConditionalOnProperty来控制Configuration是否生效