在前文我们介绍了如何使用Redis或者Caffeine来做缓存。
- Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存-2
- Spring Boot缓存实战 Caffeine
问题描述:
通过使用redis和Caffeine来做缓存,我们会发现一些问题。
- 如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多。但是使用redis横向扩展很方便。
- 如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的。
至此我们是不是有一个想法了,两个一起用。将热点数据放本地缓存(一级缓存),将非热点数据放redis缓存(二级缓存)。
缓存的选择
- 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。Caffeine 缓存详解
- 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。
数据流向
数据读取流程
数据删除流程
解决思路
Spring 本来就提供了Cache的支持,最核心的就是实现Cache和CacheManager接口。
Cache接口
主要是实现对缓存的操作,如增删查等。
public interface Cache {
String getName();
Object getNativeCache();
T get(Object key, Class type);
T get(Object key, Callable valueLoader);
void put(Object key, Object value);
ValueWrapper putIfAbsent(Object key, Object value);
void evict(Object key);
void clear();
...
}
CacheManager接口
根据缓存名称来管理Cache,核心方法就是通过缓存名称获取Cache。
public interface CacheManager {
Cache getCache(String name);
Collection getCacheNames();
}
通过上面的两个接口我的大致思路是,写一个LayeringCache来实现Cache接口,LayeringCache类中集成对Caffeine和redis的操作。
写一个LayeringCacheManager来管理LayeringCache就行了。
这里的redis缓存使用的是我扩展后的RedisCache详情请看:
- Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存,时间支持在配置文件中配置
- Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存-2。
LayeringCache
LayeringCache类,因为需要集成对Caffeine和Redis的操作,所以至少需要有name(缓存名称)、CaffeineCache和CustomizedRedisCache三个属性,还增加了一个是否使用一级缓存的开关usedFirstCache。在LayeringCache类的方法里面分别去调用操作一级缓存的和操作二级缓存的方法就可以了。
在这里特别说明一下:
- 在查询方法如get等,先去查询一级缓存,如果没查到再去查二级缓存。
- put方法没有顺序要求,但是建议将一级缓存的操作放在前面。
- 如果是删除方法如evict和clear等,需要先删掉二级缓存的数据,再去删掉一级缓存的数据,否则有并发问题。
- 删除一级缓存需要用到redis的Pub/Sub(订阅发布)模式,否则集群中其他服服务器节点的一级缓存数据无法删除。
- redis的Pub/Sub(订阅发布)模式发送消息是无状态的,如果遇到网络等原因有可能导致一些应用服务器上的一级缓存没办法删除,如果对L1和L2数据同步要求较高的话,这里可以使用MQ来做。
完整代码:
/**
* @author yuhao.wang
*/
public class LayeringCache extends AbstractValueAdaptingCache {
Logger logger = LoggerFactory.getLogger(LayeringCache.class);
/**
* 缓存的名称
*/
private final String name;
/**
* 是否使用一级缓存
*/
private boolean usedFirstCache = true;
/**
* redi缓存
*/
private final CustomizedRedisCache redisCache;
/**
* Caffeine缓存
*/
private final CaffeineCache caffeineCache;
RedisOperations extends Object, ? extends Object> redisOperations;
/**
* @param name 缓存名称
* @param prefix 缓存前缀
* @param redisOperations 操作Redis的RedisTemplate
* @param expiration redis缓存过期时间
* @param preloadSecondTime redis缓存自动刷新时间
* @param allowNullValues 是否允许存NULL,默认是false
* @param usedFirstCache 是否使用一级缓存,默认是true
* @param forceRefresh 是否强制刷新(走数据库),默认是false
* @param caffeineCache Caffeine缓存
*/
public LayeringCache(String name, byte[] prefix, RedisOperations extends Object, ? extends Object> redisOperations,
long expiration, long preloadSecondTime, boolean allowNullValues, boolean usedFirstCache,
boolean forceRefresh, com.github.benmanes.caffeine.cache.Cache
LayeringCacheManager
因为我们需要在CacheManager中来管理缓存,所以我们需要在CacheManager定义一个容器来存储缓存。在这里我们新建一个ConcurrentMap
getCache 方法
这可以说是CacheManager最核心的方法,所有CacheManager操作都围绕这个方法进行。
@Override
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
从这段逻辑我们可以看出这个方法就是根据名称获取缓存,如果没有找到并且动态创建缓存的开关dynamic为true的话,就调用createCache方法动态的创建缓存。
createCache 方法
去创建一个LayeringCache
protected Cache createCache(String name) {
return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
}
在创建缓存的时候我们会调用getSecondaryCacheExpirationSecondTime、getSecondaryCachePreloadSecondTime和getForceRefresh等方法去获取二级缓存的过期时间、自动刷新时间和是否强制刷新(走数据库)等值,这些都在secondaryCacheSettings属性中获取;调用createNativeCaffeineCache方法去创建一个一级缓存Caffeine的实例。createNativeCaffeineCache在这个方法里面会调用getCaffeine方法动态的去读取一级缓存的配置,并根据配置创建一级缓存,如果没有找到特殊配置,就使用默认配置,而这里的特殊配置则在firstCacheSettings属性中获取。
getCaffeine
动态的获取一级缓存配置,并创建对应Caffeine对象。
private Caffeine
setFirstCacheSettings和setSecondaryCacheSettings
我们借用了RedisCacheManager的setExpires(Map
/**
* 根据缓存名称设置一级缓存的有效时间和刷新时间,单位秒
*
* @param firstCacheSettings
*/
public void setFirstCacheSettings(Map firstCacheSettings) {
this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
}
/**
* 根据缓存名称设置二级缓存的有效时间和刷新时间,单位秒
*
* @param secondaryCacheSettings
*/
public void setSecondaryCacheSettings(Map secondaryCacheSettings) {
this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
}
完整代码:
/**
* @author yuhao.wang
*/
@SuppressWarnings("rawtypes")
public class LayeringCacheManager implements CacheManager {
// 常量
static final int DEFAULT_EXPIRE_AFTER_WRITE = 60;
static final int DEFAULT_INITIAL_CAPACITY = 5;
static final int DEFAULT_MAXIMUM_SIZE = 1_000;
private final ConcurrentMap cacheMap = new ConcurrentHashMap(16);
/**
* 一级缓存配置
*/
private Map firstCacheSettings = null;
/**
* 二级缓存配置
*/
private Map secondaryCacheSettings = null;
/**
* 是否允许动态创建缓存,默认是true
*/
private boolean dynamic = true;
/**
* 缓存值是否允许为NULL
*/
private boolean allowNullValues = false;
// Caffeine 属性
/**
* expireAfterWrite:60
* initialCapacity:5
* maximumSize: 1_000
*/
private Caffeine
FirstCacheSettings:
一级缓存配置类
public class FirstCacheSetting {
/**
* 一级缓存配置,配置项请点击这里 {@link CaffeineSpec#configure(String, String)}
* @param cacheSpecification
*/
public FirstCacheSetting(String cacheSpecification) {
this.cacheSpecification = cacheSpecification;
}
private String cacheSpecification;
public String getCacheSpecification() {
return cacheSpecification;
}
}
SecondaryCacheSetting:
二级缓存的特殊配置类
/**
* @author yuhao.wang
*/
public class SecondaryCacheSetting {
/**
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
*/
public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
}
/**
* @param usedFirstCache 是否启用一级缓存,默认true
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
*/
public SecondaryCacheSetting(boolean usedFirstCache, long expirationSecondTime, long preloadSecondTime) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
this.usedFirstCache = usedFirstCache;
}
/**
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
* @param forceRefresh 是否使用强制刷新(走数据库),默认false
*/
public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean forceRefresh) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
this.forceRefresh = forceRefresh;
}
/**
* @param expirationSecondTime 设置redis缓存的有效时间,单位秒
* @param preloadSecondTime 设置redis缓存的自动刷新时间,单位秒
* @param usedFirstCache 是否启用一级缓存,默认true
* @param forceRefresh 是否使用强制刷新(走数据库),默认false
*/
public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean usedFirstCache, boolean forceRefresh) {
this.expirationSecondTime = expirationSecondTime;
this.preloadSecondTime = preloadSecondTime;
this.usedFirstCache = usedFirstCache;
this.forceRefresh = forceRefresh;
}
/**
* 缓存有效时间
*/
private long expirationSecondTime;
/**
* 缓存主动在失效前强制刷新缓存的时间
* 单位:秒
*/
private long preloadSecondTime = 0;
/**
* 是否使用二级缓存,默认是true
*/
private boolean usedFirstCache = true;
/**
* 是否使用强刷新(走数据库),默认是false
*/
private boolean forceRefresh = false;
public long getPreloadSecondTime() {
return preloadSecondTime;
}
public long getExpirationSecondTime() {
return expirationSecondTime;
}
public boolean getUsedFirstCache() {
return usedFirstCache;
}
public boolean getForceRefresh() {
return forceRefresh;
}
}
使用方式
在上面我们定义好了LayeringCacheManager和LayeringCache接下来就是使用了。
新建一个配置类CacheConfig,在这里指定一个LayeringCacheManager的Bean。我那的缓存就生效了。完整代码如下:
/**
* @author yuhao.wang
*/
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig {
// redis缓存的有效时间单位是秒
@Value("${redis.default.expiration:3600}")
private long redisDefaultExpiration;
// 查询缓存有效时间
@Value("${select.cache.timeout:1800}")
private long selectCacheTimeout;
// 查询缓存自动刷新时间
@Value("${select.cache.refresh:1790}")
private long selectCacheRefresh;
@Autowired
private CacheProperties cacheProperties;
@Bean
@Primary
public CacheManager cacheManager(RedisTemplate redisTemplate) {
LayeringCacheManager layeringCacheManager = new LayeringCacheManager(redisTemplate);
// Caffeine缓存设置
setFirstCacheConfig(layeringCacheManager);
// redis缓存设置
setSecondaryCacheConfig(layeringCacheManager);
return layeringCacheManager;
}
private void setFirstCacheConfig(LayeringCacheManager layeringCacheManager) {
// 设置默认的一级缓存配置
String specification = this.cacheProperties.getCaffeine().getSpec();
if (StringUtils.hasText(specification)) {
layeringCacheManager.setCaffeineSpec(CaffeineSpec.parse(specification));
}
// 设置每个一级缓存的过期时间和自动刷新时间
Map firstCacheSettings = new HashMap<>();
firstCacheSettings.put("people", new FirstCacheSetting("initialCapacity=5,maximumSize=500,expireAfterWrite=10s"));
firstCacheSettings.put("people1", new FirstCacheSetting("initialCapacity=5,maximumSize=50,expireAfterAccess=10s"));
layeringCacheManager.setFirstCacheSettings(firstCacheSettings);
}
private void setSecondaryCacheConfig(LayeringCacheManager layeringCacheManager) {
// 设置使用缓存名称(value属性)作为redis缓存前缀
layeringCacheManager.setUsePrefix(true);
//这里可以设置一个默认的过期时间 单位是秒
layeringCacheManager.setSecondaryCacheDefaultExpiration(redisDefaultExpiration);
// 设置每个二级缓存的过期时间和自动刷新时间
Map secondaryCacheSettings = new HashMap<>();
secondaryCacheSettings.put("people", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh));
secondaryCacheSettings.put("people1", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, true));
secondaryCacheSettings.put("people2", new SecondaryCacheSetting(false, selectCacheTimeout, selectCacheRefresh));
secondaryCacheSettings.put("people3", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, false, true));
layeringCacheManager.setSecondaryCacheSettings(secondaryCacheSettings);
}
/**
* 显示声明缓存key生成器
*
* @return
*/
@Bean
public KeyGenerator keyGenerator() {
return new SimpleKeyGenerator();
}
}
在cacheManager中指定Bean的时候,我们通过调用LayeringCacheManager 的setFirstCacheSettings和setSecondaryCacheSettings方法为缓存设置一级缓存和二级缓存的特殊配置。
剩下的就是在Service方法上加注解了,如:
@Override
@Cacheable(value = "people1", key = "#person.id", sync = true)//3
public Person findOne1(Person person, String a, String[] b, List c) {
Person p = personRepository.findOne(person.getId());
logger.info("为id、key为:" + p.getId() + "数据做了缓存");
return p;
}
@Cacheable的sync属性建议设置成true。
测试
最后通过jmeter测试,50个线程,使用多级缓存,比只使用redis级缓存性能提升2倍多,只是用redis吞吐量在1243左右,使用多级缓存后在2639左右。
源码地址:
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-cache-redis-caffeine 工程
为监控而生的多级缓存框架 layering-cache这是我开源的一个多级缓存框架的实现,如果有兴趣可以看一下