springboot中使用自定义两级缓存的方法
工作中用到了springboot的缓存,使用起来挺方便的,直接引入redis或者ehcache这些缓存依赖包和相关缓存的starter依赖包,然后在启动类中加入@EnableCaching注解,然后在需要的地方就可以使用@Cacheable和@CacheEvict使用和删除缓存了。这个使用很简单,相信用过springboot缓存的都会玩,这里就不再多说了。美中不足的是,springboot使用了插件式的集成方式,虽然用起来很方便,但是当你集成ehcache的时候就是用ehcache,集成redis的时候就是用redis。如果想两者一起用,ehcache作为本地一级缓存,redis作为集成式的二级缓存,使用默认的方式据我所知是没法实现的(如果有高人可以实现,麻烦指点下我)。毕竟很多服务需要多点部署,如果单独选择ehcache可以很好地实现本地缓存,但是如果在多机之间共享缓存又需要比较费时的折腾,如果选用集中式的redis缓存,因为每次取数据都要走网络,总感觉性能不会太好。本话题主要就是讨论如何在springboot的基础上,无缝集成ehcache和redis作为一二级缓存,并且实现缓存同步。
为了不要侵入springboot原本使用缓存的方式,这里自己定义了两个缓存相关的注解,如下
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public@interfaceCacheable{
Stringvalue()default"";
Stringkey()default"";
//泛型的Class类型
Class>type()defaultException.class;
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public@interfaceCacheEvict{
Stringvalue()default"";
Stringkey()default"";
}
如上两个注解和spring中缓存的注解基本一致,只是去掉了一些不常用的属性。说到这里,不知道有没有朋友注意过,当你在springboot中单独使用redis缓存的时候,Cacheable和CacheEvict注解的value属性,实际上在redis中变成了一个zset类型的值的key,而且这个zset里面还是空的,比如@Cacheable(value="cache1",key="key1"),正常情况下redis中应该是出现cache1->map(key1,value1)这种形式,其中cache1作为缓存名称,map作为缓存的值,key作为map里的键,可以有效的隔离不同的缓存名称下的缓存。但是实际上redis里确是cache1->空(zset)和key1->value1,两个独立的键值对,试验得知不同的缓存名称下的缓存完全是共用的,如果有感兴趣的朋友可以去试验下,也就是说这个value属性实际上是个摆设,键的唯一性只由key属性保证。我只能认为这是spring的缓存实现的bug,或者是特意这么设计的,(如果有知道啥原因的欢迎指点)。
回到正题,有了注解还需要有个注解处理类,这里我使用aop的切面来进行拦截处理,原生的实现其实也大同小异。切面处理类如下:
importcom.xuanwu.apaas.core.multicache.annotation.CacheEvict;
importcom.xuanwu.apaas.core.multicache.annotation.Cacheable;
importcom.xuanwu.apaas.core.utils.JsonUtil;
importorg.apache.commons.lang3.StringUtils;
importorg.aspectj.lang.ProceedingJoinPoint;
importorg.aspectj.lang.annotation.Around;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.annotation.Pointcut;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.json.JSONArray;
importorg.json.JSONObject;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.core.LocalVariableTableParameterNameDiscoverer;
importorg.springframework.expression.ExpressionParser;
importorg.springframework.expression.spel.standard.SpelExpressionParser;
importorg.springframework.expression.spel.support.StandardEvaluationContext;
importorg.springframework.stereotype.Component;
importjava.lang.reflect.Method;
/**
*多级缓存切面
*@authorrongdi
*/
@Aspect
@Component
publicclassMultiCacheAspect{
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(MultiCacheAspect.class);
@Autowired
privateCacheFactorycacheFactory;
//这里通过一个容器初始化监听器,根据外部配置的@EnableCaching注解控制缓存开关
privatebooleancacheEnable;
@Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)")
publicvoidcacheableAspect(){
}
@Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)")
publicvoidcacheEvict(){
}
@Around("cacheableAspect()")
publicObjectcache(ProceedingJoinPointjoinPoint){
//得到被切面修饰的方法的参数列表
Object[]args=joinPoint.getArgs();
//result是方法的最终返回结果
Objectresult=null;
//如果没有开启缓存,直接调用处理方法返回
if(!cacheEnable){
try{
result=joinPoint.proceed(args);
}catch(Throwablee){
logger.error("",e);
}
returnresult;
}
//得到被代理方法的返回值类型
ClassreturnType=((MethodSignature)joinPoint.getSignature()).getReturnType();
//得到被代理的方法
Methodmethod=((MethodSignature)joinPoint.getSignature()).getMethod();
//得到被代理的方法上的注解
Cacheableca=method.getAnnotation(Cacheable.class);
//获得经过el解析后的key值
Stringkey=parseKey(ca.key(),method,args);
Class>elementClass=ca.type();
//从注解中获取缓存名称
Stringname=ca.value();
try{
//先从ehcache中取数据
StringcacheValue=cacheFactory.ehGet(name,key);
if(StringUtils.isEmpty(cacheValue)){
//如果ehcache中没数据,从redis中取数据
cacheValue=cacheFactory.redisGet(name,key);
if(StringUtils.isEmpty(cacheValue)){
//如果redis中没有数据
//调用业务方法得到结果
result=joinPoint.proceed(args);
//将结果序列化后放入redis
cacheFactory.redisPut(name,key,serialize(result));
}else{
//如果redis中可以取到数据
//将缓存中获取到的数据反序列化后返回
if(elementClass==Exception.class){
result=deserialize(cacheValue,returnType);
}else{
result=deserialize(cacheValue,returnType,elementClass);
}
}
//将结果序列化后放入ehcache
cacheFactory.ehPut(name,key,serialize(result));
}else{
//将缓存中获取到的数据反序列化后返回
if(elementClass==Exception.class){
result=deserialize(cacheValue,returnType);
}else{
result=deserialize(cacheValue,returnType,elementClass);
}
}
}catch(Throwablethrowable){
logger.error("",throwable);
}
returnresult;
}
/**
*在方法调用前清除缓存,然后调用业务方法
*@paramjoinPoint
*@return
*@throwsThrowable
*
*/
@Around("cacheEvict()")
publicObjectevictCache(ProceedingJoinPointjoinPoint)throwsThrowable{
//得到被代理的方法
Methodmethod=((MethodSignature)joinPoint.getSignature()).getMethod();
//得到被切面修饰的方法的参数列表
Object[]args=joinPoint.getArgs();
//得到被代理的方法上的注解
CacheEvictce=method.getAnnotation(CacheEvict.class);
//获得经过el解析后的key值
Stringkey=parseKey(ce.key(),method,args);
//从注解中获取缓存名称
Stringname=ce.value();
//清除对应缓存
cacheFactory.cacheDel(name,key);
returnjoinPoint.proceed(args);
}
/**
*获取缓存的key
*key定义在注解上,支持SPEL表达式
*@return
*/
privateStringparseKey(Stringkey,Methodmethod,Object[]args){
if(StringUtils.isEmpty(key))returnnull;
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscovereru=newLocalVariableTableParameterNameDiscoverer();
String[]paraNameArr=u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParserparser=newSpelExpressionParser();
//SPEL上下文
StandardEvaluationContextcontext=newStandardEvaluationContext();
//把方法参数放入SPEL上下文中
for(inti=0;i
privateObjectdeserialize(Stringstr,Classclazz,ClasselementClass){
Objectresult=null;
try{
if(clazz==JSONObject.class){
result=newJSONObject(str);
}elseif(clazz==JSONArray.class){
result=newJSONArray(str);
}else{
result=JsonUtil.deserialize(str,clazz,elementClass);
}
}catch(Exceptione){
}
returnresult;
}
publicvoidsetCacheEnable(booleancacheEnable){
this.cacheEnable=cacheEnable;
}
}
上面这个界面使用了一个cacheEnable变量控制是否使用缓存,为了实现无缝的接入springboot,必然需要受到原生@EnableCaching注解的控制,这里我使用一个spring容器加载完成的监听器,然后在监听器里找到是否有被@EnableCaching注解修饰的类,如果有就从spring容器拿到MultiCacheAspect对象,然后将cacheEnable设置成true。这样就可以实现无缝接入springboot,不知道朋友们还有没有更加优雅的方法呢?欢迎交流!监听器类如下
importcom.xuanwu.apaas.core.multicache.CacheFactory; importcom.xuanwu.apaas.core.multicache.MultiCacheAspect; importorg.springframework.cache.annotation.EnableCaching; importorg.springframework.context.ApplicationListener; importorg.springframework.context.event.ContextRefreshedEvent; importorg.springframework.stereotype.Component; importjava.util.Map; /** *用于spring加载完成后,找到项目中是否有开启缓存的注解@EnableCaching *@authorrongdi */ @Component publicclassContextRefreshedListenerimplementsApplicationListener{ @Override publicvoidonApplicationEvent(ContextRefreshedEventevent){ //判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次) if(event.getApplicationContext().getParent()==null){ //得到所有被@EnableCaching注解修饰的类 Map beans=event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class); if(beans!=null&&!beans.isEmpty()){ MultiCacheAspectmultiCache=(MultiCacheAspect)event.getApplicationContext().getBean("multiCacheAspect"); multiCache.setCacheEnable(true); } } } }
实现了无缝接入,还需要考虑多点部署的时候,多点的ehcache怎么和redis缓存保持一致的问题。在正常应用中,一般redis适合长时间的集中式缓存,ehcache适合短时间的本地缓存,假设现在有A,B和C服务器,A和B部署了业务服务,C部署了redis服务。当请求进来,前端入口不管是用LVS或者nginx等负载软件,请求都会转发到某一个具体服务器,假设转发到了A服务器,修改了某个内容,而这个内容在redis和ehcache中都有,这时候,A服务器的ehcache缓存,和C服务器的redis不管控制缓存失效也好,删除也好,都比较容易,但是这时候B服务器的ehcache怎么控制失效或者删除呢?一般比较常用的方式就是使用发布订阅模式,当需要删除缓存的时候在一个固定的通道发布一个消息,然后每个业务服务器订阅这个通道,收到消息后删除或者过期本地的ehcache缓存(最好是使用过期,但是redis目前只支持对key的过期操作,没办法操作key下的map里的成员的过期,如果非要强求用过期,可以自己加时间戳自己实现,不过用删除出问题的几率也很小,毕竟加缓存的都是读多写少的应用,这里为了方便都是直接删除缓存)。总结起来流程就是更新某条数据,先删除redis中对应的缓存,然后发布一个缓存失效的消息在redis的某个通道中,本地的业务服务去订阅这个通道的消息,当业务服务收到这个消息后去删除本地对应的ehcache缓存,redis的各种配置如下
importcom.fasterxml.jackson.annotation.JsonAutoDetect;
importcom.fasterxml.jackson.annotation.PropertyAccessor;
importcom.fasterxml.jackson.databind.ObjectMapper;
importcom.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber;
importorg.springframework.cache.CacheManager;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.data.redis.cache.RedisCacheManager;
importorg.springframework.data.redis.connection.RedisConnectionFactory;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.data.redis.core.StringRedisTemplate;
importorg.springframework.data.redis.listener.PatternTopic;
importorg.springframework.data.redis.listener.RedisMessageListenerContainer;
importorg.springframework.data.redis.listener.adapter.MessageListenerAdapter;
importorg.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
@Configuration
publicclassRedisConfig{
@Bean
publicCacheManagercacheManager(RedisTemplateredisTemplate){
RedisCacheManagerrcm=newRedisCacheManager(redisTemplate);
//设置缓存过期时间(秒)
rcm.setDefaultExpiration(600);
returnrcm;
}
@Bean
publicRedisTemplateredisTemplate(RedisConnectionFactoryfactory){
StringRedisTemplatetemplate=newStringRedisTemplate(factory);
Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);
ObjectMapperom=newObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
returntemplate;
}
/**
*redis消息监听器容器
*可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
*通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
*@paramconnectionFactory
*@paramlistenerAdapter
*@return
*/
@Bean
publicRedisMessageListenerContainercontainer(RedisConnectionFactoryconnectionFactory,
MessageListenerAdapterlistenerAdapter){
RedisMessageListenerContainercontainer=newRedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
//订阅了一个叫redis.uncache的通道
container.addMessageListener(listenerAdapter,newPatternTopic("redis.uncache"));
//这个container可以添加多个messageListener
returncontainer;
}
/**
*消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
*@paramreceiver
*@return
*/
@Bean
MessageListenerAdapterlistenerAdapter(MessageSubscriberreceiver){
//这个地方是给messageListenerAdapter传入一个消息接受的处理器,利用反射的方法调用“handle”
returnnewMessageListenerAdapter(receiver,"handle");
}
}
消息发布类如下:
importcom.xuanwu.apaas.core.multicache.CacheFactory;
importorg.apache.commons.lang3.StringUtils;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Component;
@Component
publicclassMessageSubscriber{
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(MessageSubscriber.class);
@Autowired
privateCacheFactorycacheFactory;
/**
*接收到redis订阅的消息后,将ehcache的缓存失效
*@parammessage格式为name_key
*/
publicvoidhandle(Stringmessage){
logger.debug("redis.ehcache:"+message);
if(StringUtils.isEmpty(message)){
return;
}
String[]strs=message.split("#");
Stringname=strs[0];
Stringkey=null;
if(strs.length==2){
key=strs[1];
}
cacheFactory.ehDel(name,key);
}
}
具体操作缓存的类如下:
importcom.xuanwu.apaas.core.multicache.publisher.MessagePublisher;
importnet.sf.ehcache.Cache;
importnet.sf.ehcache.CacheManager;
importnet.sf.ehcache.Element;
importorg.apache.commons.lang3.StringUtils;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.data.redis.RedisConnectionFailureException;
importorg.springframework.data.redis.core.HashOperations;
importorg.springframework.data.redis.core.RedisTemplate;
importorg.springframework.stereotype.Component;
importjava.io.InputStream;
/**
*多级缓存切面
*@authorrongdi
*/
@Component
publicclassCacheFactory{
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(CacheFactory.class);
@Autowired
privateRedisTemplateredisTemplate;
@Autowired
privateMessagePublishermessagePublisher;
privateCacheManagercacheManager;
publicCacheFactory(){
InputStreamis=this.getClass().getResourceAsStream("/ehcache.xml");
if(is!=null){
cacheManager=CacheManager.create(is);
}
}
publicvoidcacheDel(Stringname,Stringkey){
//删除redis对应的缓存
redisDel(name,key);
//删除本地的ehcache缓存,可以不需要,订阅器那里会删除
//ehDel(name,key);
if(cacheManager!=null){
//发布一个消息,告诉订阅的服务该缓存失效
messagePublisher.publish(name,key);
}
}
publicStringehGet(Stringname,Stringkey){
if(cacheManager==null)returnnull;
Cachecache=cacheManager.getCache(name);
if(cache==null)returnnull;
cache.acquireReadLockOnKey(key);
try{
Elementele=cache.get(key);
if(ele==null)returnnull;
return(String)ele.getObjectValue();
}finally{
cache.releaseReadLockOnKey(key);
}
}
publicStringredisGet(Stringname,Stringkey){
HashOperationsoper=redisTemplate.opsForHash();
try{
returnoper.get(name,key);
}catch(RedisConnectionFailureExceptione){
//连接失败,不抛错,直接不用redis缓存了
logger.error("connectrediserror",e);
returnnull;
}
}
publicvoidehPut(Stringname,Stringkey,Stringvalue){
if(cacheManager==null)return;
if(!cacheManager.cacheExists(name)){
cacheManager.addCache(name);
}
Cachecache=cacheManager.getCache(name);
//获得key上的写锁,不同key互相不影响,类似于synchronized(key.intern()){}
cache.acquireWriteLockOnKey(key);
try{
cache.put(newElement(key,value));
}finally{
//释放写锁
cache.releaseWriteLockOnKey(key);
}
}
publicvoidredisPut(Stringname,Stringkey,Stringvalue){
HashOperationsoper=redisTemplate.opsForHash();
try{
oper.put(name,key,value);
}catch(RedisConnectionFailureExceptione){
//连接失败,不抛错,直接不用redis缓存了
logger.error("connectrediserror",e);
}
}
publicvoidehDel(Stringname,Stringkey){
if(cacheManager==null)return;
if(cacheManager.cacheExists(name)){
//如果key为空,直接根据缓存名删除
if(StringUtils.isEmpty(key)){
cacheManager.removeCache(name);
}else{
Cachecache=cacheManager.getCache(name);
cache.remove(key);
}
}
}
publicvoidredisDel(Stringname,Stringkey){
HashOperationsoper=redisTemplate.opsForHash();
try{
//如果key为空,直接根据缓存名删除
if(StringUtils.isEmpty(key)){
redisTemplate.delete(name);
}else{
oper.delete(name,key);
}
}catch(RedisConnectionFailureExceptione){
//连接失败,不抛错,直接不用redis缓存了
logger.error("connectrediserror",e);
}
}
}
工具类如下
importcom.fasterxml.jackson.core.type.TypeReference;
importcom.fasterxml.jackson.databind.DeserializationFeature;
importcom.fasterxml.jackson.databind.JavaType;
importcom.fasterxml.jackson.databind.ObjectMapper;
importorg.apache.commons.lang3.StringUtils;
importorg.json.JSONArray;
importorg.json.JSONObject;
importjava.util.*;
publicclassJsonUtil{
privatestaticObjectMappermapper;
static{
mapper=newObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
}
/**
*将对象序列化成json
*
*@paramobj待序列化的对象
*@return
*@throwsException
*/
publicstaticStringserialize(Objectobj)throwsException{
if(obj==null){
thrownewIllegalArgumentException("objshouldnotbenull");
}
returnmapper.writeValueAsString(obj);
}
/**
带泛型的反序列化,比如一个JSONArray反序列化成List
*/
publicstaticTdeserialize(StringjsonStr,Class>collectionClass,
Class>...elementClasses)throwsException{
JavaTypejavaType=mapper.getTypeFactory().constructParametrizedType(
collectionClass,collectionClass,elementClasses);
returnmapper.readValue(jsonStr,javaType);
}
/**
*将json字符串反序列化成对象
*@paramsrc待反序列化的json字符串
*@paramt反序列化成为的对象的class类型
*@return
*@throwsException
*/
publicstaticTdeserialize(Stringsrc,Classt)throwsException{
if(src==null){
thrownewIllegalArgumentException("srcshouldnotbenull");
}
if("{}".equals(src.trim())){
returnnull;
}
returnmapper.readValue(src,t);
}
}
具体使用缓存,和之前一样只需要关注@Cacheable和@CacheEvict注解,同样也支持spring的el表达式。而且这里的value属性表示的缓存名称也没有上面说的那个问题,完全可以用value隔离不同的缓存,例子如下
@Cacheable(value="bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode") @CacheEvict(value="bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
附上主要的依赖包
- "org.springframework.boot:spring-boot-starter-redis:1.4.2.RELEASE",
- 'net.sf.ehcache:ehcache:2.10.4',
- "org.json:json:20160810"
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。