最近在项目中发现公司通过caffeine和redis实现二级缓存,redis大家都比较熟悉了,caffeine有点新鲜。所以自己找了下资料发现spring cache支持的缓存类型就有caffeine,关于caffeine我就不再具体的介绍了,感兴趣的小伙伴可以到网上搜索一下具体资料。spring cache关于caffeine的可以参考文档。使用起来还是比较简单的,下面通过caffeine和redis来自己实现一个二级缓存。另外网上也有一些实现的相关文章,比如下面这两篇:
Spring Boot缓存实战 Redis + Caffeine 实现多级缓存
Sprboot + spring cache implements two-level caching (redis + caffeine)
不过在实现方式上会有略微的差别,下面开始自己实现。
一、自定义Cache
我们这里主要是自定义二级缓存,所以我们需要定义一个自己的缓存类,这个缓存类包括了两个成员变量,即一级缓存和二级缓存,简单点理解就是自定义的缓存是将caffeine和redis的缓存又进行了封装,代码如下:
public class CaffeineRedisCache implements Cache {
private String cacheName;
// 一级缓存
private Cache firstLevel;
// 二级缓存
private Cache secondLevel;
public CaffeineRedisCache(String cacheName, Cache first, Cache second) {
log.info(">>>> CaffeineRedisCache constructor start,params:cacheName={},firstCache={},secondCache={} <<<<",cacheName,first,second);
this.cacheName = cacheName;
this.firstLevel = first;
this.secondLevel = second;
}
// 省略相关方法
......
}
这里是去实现Cache
接口或者继承AbstractValueAdaptingCache
都是可以的。重写相关的方法即可。
这里只是定义了缓存类,那么这个缓存类的创建需要CacheManager
来完成,所以我们还需要新建一个自定的CacheManager
。
二、自定义CacheManger
自定义CacheManager
也是一样的,不管是实现接口还是继承相关的类,主要目的就是创建Cache
,我这里直接实现CacheManager
接口。这里我自己添加了一个类型为CacheManagerContainer
(自定义辅助类,代码见下面的辅助类代码)的成员变量,它的作用其实比较简单,就为了维护需要用到的其他CacheManger
,比如RedisCacheManager
、CaffeineCacheManager
等,以及自定义配置类,因为是自定义缓存,所以原来spring cache
的配置不能再使用(自定义配置类见下),CacheManager
代码如下:
public class CaffeineRedisCacheManager implements CacheManager {
private CacheManagerContainer cacheManagerContainer;
public CaffeineRedisCacheManager(CacheManagerContainer cacheManagerContainer) {
this.cacheManagerContainer = cacheManagerContainer;
}
@Override
public Cache getCache(String name) {
CacheManagerContainer.CacheManagers containers = cacheManagerContainer.getManagers(name);
return new CaffeineRedisCache(name,containers.getLevelOne().getCache(name),containers.getLevelTwo().getCache(name));
}
@Override
public Collection getCacheNames() {
String cacheName = cacheManagerContainer.getCustomCacheProperties().getCacheName();
List cacheNames = new ArrayList<>();
cacheNames.add(cacheName);
return cacheNames;
}
}
上面代码只是粗略的实现,有些地方还有待优化之处。通过getCache方法,就会返回自定义的CaffeineRedisCache
。而CacheManagerContainer
通过其维护的相关CacheManager
获取对应的Cache
。
三、自定义配置类
自定义配置类是因为我们需要指定缓存的类型、过期时间等等。关于缓存的类型根据自己的需要定义(见下辅助类代码的CacheType)。
自定义配置目前只有5种可配置项。其中主要说下配置中的Map,这一点主要是为了更加灵活的使用自定义缓存,比如某种类型的数据只缓存redis,另外一种类型只缓存caffeine等等。所以添加了一个内部类Container
,简单理解Container
控制缓存的粒度更细。代码如下:
@Configuration
@ConfigurationProperties(prefix = "com.ypc.custom.cache")
public class CustomCacheProperties {
// 缓存名称
private String cacheName;
// 默认缓存类型
private CacheType defaultCacheType;
// 缓存多少秒过期,默认为0,不过期
private Integer expireSecond;
private Map containers;
// 是否缓存null值
private boolean cacheNullValues;
public CustomCacheProperties() {
this.defaultCacheType = CacheType.CAFFEINE_REDIS;
this.expireSecond = 0;
this.containers = new HashMap<>();
this.cacheNullValues = false;
}
// 省略get set方法
......
// 内部类,主要为了细化具体的缓存
public static class Container {
// 缓存类型
private CacheType cacheType;
// 最大大小
private Integer maximumSize;
// 初始容量
private Integer initialCapacity;
// 过期时间
private Integer expireAfterAccess;
public Container() {
}
//省略get set方法
......
}
四、辅助类代码
辅助类主要是自定义的缓存类型,这里我定义了4种,分别表示不使用缓存、使用redis、使用caffeine、同时使用redis和caffeine。
public enum CacheType {
NONE,
REDIS,
CAFFEINE,
CAFFEINE_REDIS;
private CacheType() {
}
}
另一个辅助类也是比较重要的,这个缓存类主要是在使用根据缓存类型获取到具体的CacheManager
,这里需要理解一下。也就是说自定义使用何种缓存就应该返回对应的CacheManager
。因为我们是自定义了二级缓存,每一及缓存都有对应的缓存处理器。比如缓存类型是REDIS,则缓存处理器应该是redisCacheManager
和noneCacheManager
。如果缓存类型是 CAFFEINE_REDIS,那么缓存处理器则切换成caffeineCacheManager
(即自定义的CustomCaffeineCacheManager
)和redisCacheManager
。
public class CacheManagerContainer {
private CacheManager redisCacheManager;
private CacheManager caffeineCacheManager;
private CacheManager noneCacheManager;
private CustomCacheProperties customCacheProperties;
public CustomCacheProperties getCustomCacheProperties() {
return customCacheProperties;
}
private Map cacheTypeMap;
public CacheManagerContainer() {
}
public CacheManagerContainer(CacheManager redisCacheManager, CacheManager caffeineCacheManager, CustomCacheProperties customCacheProperties) {
this.redisCacheManager = redisCacheManager;
this.caffeineCacheManager = caffeineCacheManager;
this.noneCacheManager = new NoOpCacheManager();
this.customCacheProperties = customCacheProperties;
this.cacheTypeMap = new HashMap<>();
for (Map.Entry entry : customCacheProperties.getContainers().entrySet()) {
String key = (String) entry.getKey();
CustomCacheProperties.Container container = (CustomCacheProperties.Container) entry.getValue();
CacheType cacheType = container.getCacheType() == null ? customCacheProperties.getDefaultCacheType() : container.getCacheType();
this.cacheTypeMap.put(key,cacheType);
}
}
public CacheManagers getManagers(String name) {
CacheManagers cacheManagers = null;
CacheType cacheType = this.cacheTypeMap.get(name);
if (CAFFEINE_REDIS.equals(cacheType)) {
cacheManagers = new CacheManagerContainer.CacheManagers(caffeineCacheManager,redisCacheManager);
} else if (CAFFEINE.equals(cacheType)) {
cacheManagers = new CacheManagerContainer.CacheManagers(caffeineCacheManager,noneCacheManager);
} else if (REDIS.equals(cacheType)){
cacheManagers = new CacheManagerContainer.CacheManagers(redisCacheManager,noneCacheManager);
} else {
cacheManagers = new CacheManagerContainer.CacheManagers(noneCacheManager,noneCacheManager);
}
return cacheManagers;
}
public static class CacheManagers {
private CacheManager levelOne;
private CacheManager levelTwo;
public CacheManagers(CacheManager firstLevel, CacheManager secondLevel) {
this.levelOne = firstLevel;
this.levelTwo = secondLevel;
}
public CacheManager getLevelOne() {
return levelOne;
}
public CacheManager getLevelTwo() {
return levelTwo;
}
}
}
这个类的内部类CacheManagers
维护了所用到的CacheManager
。通过CacheManagerContainer
构造函数,将自定义配置的缓存类型注入到其成员变量cacheTypeMap
,这样就可以根据不同的缓存类型最终返回不同的CacheManagers
,从而返回到真正的CacheManager
。
五、配置相关bean
这里主要是注入自定义的相关bean,比如redisCacheManager
、KeyGenerator
等等。这些bean有些需要根据自定义配置类来设置,所以注入了自定义的配置类,比如缓存名称、缓存的时间等等。这里主要说明两点,一个是自定义的caffeineRedisCacheManager
,这个是bean以来redisCacheManager
和caffeineCacheManager
,因为需要多个CacheManager
,所以这里调用了一个自定义的辅助类CacheManagerContainer
,这个类维护了多个CacheManager
,这个会根据配置的缓存类型返回不同的CacheManager
。
另一个是caffeineCacheManager
,这个是我们自定义的一个CacheManager
,之所以没有使用默认的CaffeineCacheManager
是因为上面说过的,为了缓存的灵活性(即有多个container),可以理解为在自定义的caffeineCacheManager
内部维护了多个Cache
。
@Slf4j
@Configuration
@EnableConfigurationProperties({CustomCacheProperties.class})
public class CaffeineRedisCacheConfig {
@Autowired
private CustomCacheProperties customCacheProperties;
/**
* 自定义RedisCacheManger
* @param redisConnectionFactory
* @return
*/
@Bean("redisCacheManager")
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
log.info(">>>> create redisCacheManager bean start <<<<");
Integer expireSecond = customCacheProperties.getExpireSecond();
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 指定key value的序列化类型
RedisSerializer
redisCacheManager
的序列化方式使用的是默认的JdkSerializationRedisSerializer
,本来自己打算使用json进行序列化,但是在使用过程中出现了一点问题,这个问题一直也没解决,所以就先用默认序列化方式了。
下面是自定义CaffeineCacheManager
代码:
public class CustomCaffeineCacheManager extends CaffeineCacheManager {
private Map> map = new HashMap();
private boolean cacheNullValues;
@Override
protected org.springframework.cache.Cache createCaffeineCache(String name) {
log.info(">>>> create CaffeineCache by name={} <<<<",name);
return new CaffeineCache(name,createNativeCaffeineCache(name),cacheNullValues);
}
public CustomCaffeineCacheManager(Map> map,boolean cacheNullValues) {
log.info(">>>> CustomCaffeineCacheManager constructor start <<<<");
this.map = map;
this.cacheNullValues = cacheNullValues;
super.setCacheNames(map.keySet());
}
protected Cache createNativeCaffeineCache(String name) {
return map.get(name);
}
}
自定义的CustomCaffeineCacheManager
继承了CaffeineCacheManager
,并重写了createCaffeineCache方法。
六、测试
下面我将自定义的缓存类代码完整的贴一下,添加了很多日志,主要是为了显示具体缓存是走的Caffeine
还是Redis
,代码如下:
public class CaffeineRedisCache implements Cache {
private String cacheName;
// 一级缓存
private Cache firstLevel;
// 二级缓存
private Cache secondLevel;
public CaffeineRedisCache(String cacheName, Cache first, Cache second) {
log.info(">>>> CaffeineRedisCache constructor start,params:cacheName={},firstCache={},secondCache={} <<<<",cacheName,first,second);
this.cacheName = cacheName;
this.firstLevel = first;
this.secondLevel = second;
}
@Override
public String getName() {
return cacheName;
}
@Override
public Object getNativeCache() {
log.info(">>>> getNativeCache method <<<<");
return this;
}
@Override
public ValueWrapper get(Object key) {
log.info(">>>> get method key={} <<<<",key);
ValueWrapper value = firstLevel.get(key);
if (value == null) {
// 二级缓存
log.info(">>>> get cache object from second level <<<<");
value = secondLevel.get(key);
if (value != null) {
Object result = value.get();
firstLevel.put(key,result);
}
}
return value;
}
@Override
public T get(Object key, Class clazz) {
log.info(">>>> get class method key={},clazz={} <<<<",key,clazz);
T value = firstLevel.get(key, clazz);
if (value == null) {
log.info(">>>> get cache object from second level <<<<");
value = secondLevel.get(key,clazz);
if (value != null) {
firstLevel.put(key,value);
}
}
return value;
}
@Override
public T get(Object key, Callable callable) {
log.info(">>>> get callable method key={},callable={} <<<<",key,callable);
T result = null;
result = firstLevel.get(key,callable);
if (result != null)
return result;
else {
return secondLevel.get(key,callable);
}
}
@Override
public void put(Object key, Object value) {
log.info(">>>> put method,key={},value={} <<<<",key,value);
firstLevel.put(key,value);
secondLevel.put(key,value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
log.info(">>>> putIfAbsent method,key={},value={} <<<<",key,value);
firstLevel.putIfAbsent(key,value);
return secondLevel.putIfAbsent(key,value);
}
@Override
public void evict(Object o) {
secondLevel.evict(o);
firstLevel.evict(o);
}
@Override
public void clear() {
secondLevel.clear();
firstLevel.clear();
}
}
另外application.properties的自定义配置如下:
server.port=8090
# 数据库配置
spring.datasource.url=jdbc:postgresql://localhost:5432/pgsql?useSSL=false&characterEncoding=utf8
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=123456
# 自定义缓存配置
com.ypc.custom.cache.cacheName=cache_custom
com.ypc.custom.cache.cacheNullValues=true
com.ypc.custom.cache.expireSecond=10000
com.ypc.custom.cache.defaultCacheType=REDIS
# 自定义缓存容器配置
com.ypc.custom.cache.containers.caffeine_cache.cacheType=CAFFEINE_REDIS
com.ypc.custom.cache.containers.caffeine_cache.maximumSize=500
com.ypc.custom.cache.containers.caffeine_cache.initialCapacity=100
com.ypc.custom.cache.containers.caffeine_cache.expireAfterAccess=6000
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.show-sql=true
配置中默认使用的缓存是REDIS,但是caffeine_cache
容器配置的类型是CAFFEINE_REDIS,也就是会使用caffeine作为一级缓存,redis做为二级缓存。这个容器名称需要和使用@Cacheable
注解的缓存名称保持一致。
到这里代码层面的东西已经差不多完成了,当然其实过程没有这么简单,这个过程中遇到了不少的问题,即使现在感觉还是比较混乱的,有机会的话代码还是要好好修改一下。下面通过一个简单的接口测试下自定义的缓存有没有生效。
第一次访问时日志输出如下:
2019-08-04 18:00:12.908 INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache : >>>> CaffeineRedisCache constructor start,params:cacheName=caffeine_cache,firstCache=org.springframework.cache.caffeine.CaffeineCache@54ea6172,secondCache=org.springframework.data.redis.cache.RedisCache@570f2df3 <<<<
2019-08-04 18:00:14.548 INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache : >>>> get method key=caffeine <<<<
2019-08-04 18:00:14.549 INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache : >>>> get cache object from second level <<<<
2019-08-04 18:00:14.598 INFO 18468 --- [nio-8090-exec-3] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.sex as sex3_0_, user0_.userid as userid4_0_, user0_.username as username5_0_ from t_user user0_ where user0_.username=?
2019-08-04 18:00:14.682 INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache : >>>> put method,key=caffeine,value=User{id=16, age=212, username='caffeine', sex='male', userid='54545'} <<<<
日志第一行输出的是CaffeineRedisCache
构造函数的执行,然后分别从一级缓存和二级缓存查找结果,因为缓存没有找到,所以这里输出了具体的sql,然后调用了put方法,将结果放入一级和二级缓存。
接下来再次调用这个接口,日志输出如下:
2019-08-04 18:05:10.223 INFO 18468 --- [nio-8090-exec-5] c.y.s.c.cache.CaffeineRedisCache : >>>> CaffeineRedisCache constructor start,params:cacheName=caffeine_cache,firstCache=org.springframework.cache.caffeine.CaffeineCache@54ea6172,secondCache=org.springframework.data.redis.cache.RedisCache@570f2df3 <<<<
2019-08-04 18:05:10.936 INFO 18468 --- [nio-8090-exec-5] c.y.s.c.cache.CaffeineRedisCache : >>>> get method key=caffeine <<<<
同样先执行CaffeineRedisCache
构造函数,接着直接调用get方法,这时候是从一级缓存中获取到了结果,直接返回。通过工具看下redis里面的数据,图片如下:
因为使用的是jdk的序列化方式,所以看着没那么直观,说明自定义的缓存是成功的。这里有点不太满意的地方就是指定redis缓存的序列化方式为json一直是失败的,不管是
Jackson2JsonRedisSerializer
还是GenericJackson2JsonRedisSerializer
(RedisSerializer.json()
)网找找了一些资料,但是最终还是没有解决问题,所以最后改成了jdk的序列化策略。
好了关于使用caffeine和redis实现二级缓存就先到这里,中间自己也是踩了不少的坑,最后算是勉强完成(redis缓存使用json序列化有时间再看下吧),不过这次学习对spring cache
方面的了解又多了一层,感觉还是很有收获的。就实现来讲这部分并不算难,无非是将redis和caffeine的缓存结合起来使用而已,开头的两篇文章也是非常不错的。这次的代码我已经提交到github。
更正:经测试redis使用json序列化方式是可行的,但是使用Jackson2JsonRedisSerializer
依然不可行,会报下列错误:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to ***.**.***.***.
使用GenericJackson2JsonRedisSerializer
(RedisSerializer.json()
)是可行的。