温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么在springboot中自定义两级缓存

发布时间:2021-05-17 17:14:04 来源:亿速云 阅读:151 作者:Leah 栏目:编程语言

本篇文章给大家分享的是有关怎么在springboot中自定义两级缓存,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

  @Target({ElementType.METHOD})   @Retention(RetentionPolicy.RUNTIME)   public @interface Cacheable {     String value() default "";     String key() default "";     //泛型的Class类型     Class<?> type() default Exception.class;   }      @Target({ElementType.METHOD})   @Retention(RetentionPolicy.RUNTIME)   public @interface CacheEvict {     String value() default "";     String key() 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的切面来进行拦截处理,原生的实现其实也大同小异。切面处理类如下:

  import com.xuanwu.apaas.core.multicache.annotation.CacheEvict;   import com.xuanwu.apaas.core.multicache.annotation.Cacheable;   import com.xuanwu.apaas.core.utils.JsonUtil;   import org.apache.commons.lang3.StringUtils;   import org.aspectj.lang.ProceedingJoinPoint;   import org.aspectj.lang.annotation.Around;   import org.aspectj.lang.annotation.Aspect;   import org.aspectj.lang.annotation.Pointcut;   import org.aspectj.lang.reflect.MethodSignature;   import org.json.JSONArray;   import org.json.JSONObject;   import org.slf4j.Logger;   import org.slf4j.LoggerFactory;   import org.springframework.beans.factory.annotation.Autowired;   import org.springframework.core.LocalVariableTableParameterNameDiscoverer;   import org.springframework.expression.ExpressionParser;   import org.springframework.expression.spel.standard.SpelExpressionParser;   import org.springframework.expression.spel.support.StandardEvaluationContext;   import org.springframework.stereotype.Component;   import java.lang.reflect.Method;   /**    * 多级缓存切面    * @author rongdi    */   @Aspect   @Component   public class MultiCacheAspect {     private static final Logger logger = LoggerFactory.getLogger(MultiCacheAspect.class);     @Autowired     private CacheFactory cacheFactory;     //这里通过一个容器初始化监听器,根据外部配置的@EnableCaching注解控制缓存开关     private boolean cacheEnable;     @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)")     public void cacheableAspect() {     }     @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)")     public void cacheEvict() {     }     @Around("cacheableAspect()")     public Object cache(ProceedingJoinPoint joinPoint) {       //得到被切面修饰的方法的参数列表       Object[] args = joinPoint.getArgs();       // result是方法的最终返回结果       Object result = null;       //如果没有开启缓存,直接调用处理方法返回       if(!cacheEnable){         try {           result = joinPoint.proceed(args);         } catch (Throwable e) {           logger.error("",e);         }         return result;       }       // 得到被代理方法的返回值类型       Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();       // 得到被代理的方法       Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();       // 得到被代理的方法上的注解       Cacheable ca = method.getAnnotation(Cacheable.class);       //获得经过el解析后的key值       String key = parseKey(ca.key(),method,args);       Class<?> elementClass = ca.type();       //从注解中获取缓存名称       String name = ca.value();       try {         //先从ehcache中取数据         String cacheValue = 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 (Throwable throwable) {         logger.error("",throwable);       }       return result;     }     /**      * 在方法调用前清除缓存,然后调用业务方法      * @param joinPoint      * @return      * @throws Throwable      *      */     @Around("cacheEvict()")     public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable {       // 得到被代理的方法       Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();       //得到被切面修饰的方法的参数列表       Object[] args = joinPoint.getArgs();       // 得到被代理的方法上的注解       CacheEvict ce = method.getAnnotation(CacheEvict.class);       //获得经过el解析后的key值       String key = parseKey(ce.key(),method,args);       //从注解中获取缓存名称       String name = ce.value();       // 清除对应缓存       cacheFactory.cacheDel(name,key);       return joinPoint.proceed(args);     }     /**      * 获取缓存的key      * key 定义在注解上,支持SPEL表达式      * @return      */     private String parseKey(String key,Method method,Object [] args){       if(StringUtils.isEmpty(key)) return null;       //获取被拦截方法参数名列表(使用Spring支持类库)       LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();       String[] paraNameArr = u.getParameterNames(method);       //使用SPEL进行key的解析       ExpressionParser parser = new SpelExpressionParser();       //SPEL上下文       StandardEvaluationContext context = new StandardEvaluationContext();       //把方法参数放入SPEL上下文中       for(int i=0;i<paraNameArr.length;i++){         context.setVariable(paraNameArr[i], args[i]);       }       return parser.parseExpression(key).getValue(context,String.class);     }     //序列化     private String serialize(Object obj) {       String result = null;       try {         result = JsonUtil.serialize(obj);       } catch(Exception e) {         result = obj.toString();       }       return result;     }     //反序列化     private Object deserialize(String str,Class clazz) {       Object result = null;       try {         if(clazz == JSONObject.class) {           result = new JSONObject(str);         } else if(clazz == JSONArray.class) {           result = new JSONArray(str);         } else {           result = JsonUtil.deserialize(str,clazz);         }       } catch(Exception e) {       }       return result;     }     //反序列化,支持List<xxx>     private Object deserialize(String str,Class clazz,Class elementClass) {       Object result = null;       try {         if(clazz == JSONObject.class) {           result = new JSONObject(str);         } else if(clazz == JSONArray.class) {           result = new JSONArray(str);         } else {           result = JsonUtil.deserialize(str,clazz,elementClass);         }       } catch(Exception e) {       }       return result;     }     public void setCacheEnable(boolean cacheEnable) {       this.cacheEnable = cacheEnable;     }   }

上面这个界面使用了一个cacheEnable变量控制是否使用缓存,为了实现无缝的接入springboot,必然需要受到原生@EnableCaching注解的控制,这里我使用一个spring容器加载完成的监听器,然后在监听器里找到是否有被@EnableCaching注解修饰的类,如果有就从spring容器拿到MultiCacheAspect对象,然后将cacheEnable设置成true。这样就可以实现无缝接入springboot,不知道朋友们还有没有更加优雅的方法呢?欢迎交流!监听器类如下

  import com.xuanwu.apaas.core.multicache.CacheFactory;   import com.xuanwu.apaas.core.multicache.MultiCacheAspect;   import org.springframework.cache.annotation.EnableCaching;   import org.springframework.context.ApplicationListener;   import org.springframework.context.event.ContextRefreshedEvent;   import org.springframework.stereotype.Component;   import java.util.Map;   /**    * 用于spring加载完成后,找到项目中是否有开启缓存的注解@EnableCaching    * @author rongdi    */   @Component   public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {         @Override      public void onApplicationEvent(ContextRefreshedEvent event) {        // 判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次)       if(event.getApplicationContext().getParent()==null){         //得到所有被@EnableCaching注解修饰的类         Map<String,Object> beans = event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class);         if(beans != null && !beans.isEmpty()) {           MultiCacheAspect multiCache = (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的各种配置如下

  import com.fasterxml.jackson.annotation.JsonAutoDetect;   import com.fasterxml.jackson.annotation.PropertyAccessor;   import com.fasterxml.jackson.databind.ObjectMapper;   import com.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber;   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.connection.RedisConnectionFactory;   import org.springframework.data.redis.core.RedisTemplate;   import org.springframework.data.redis.core.StringRedisTemplate;   import org.springframework.data.redis.listener.PatternTopic;   import org.springframework.data.redis.listener.RedisMessageListenerContainer;   import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;   import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;   @Configuration   public class RedisConfig {     @Bean     public CacheManager cacheManager(RedisTemplate redisTemplate) {      RedisCacheManager rcm = new RedisCacheManager(redisTemplate);      //设置缓存过期时间(秒)      rcm.setDefaultExpiration(600);      return rcm;     }     @Bean     public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {      StringRedisTemplate template = new StringRedisTemplate(factory);      Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);      ObjectMapper om = new ObjectMapper();      om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);      om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);      jackson2JsonRedisSerializer.setObjectMapper(om);      template.setValueSerializer(jackson2JsonRedisSerializer);      template.afterPropertiesSet();      return template;     }     /**     * redis消息监听器容器     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理     * @param connectionFactory     * @param listenerAdapter     * @return     */     @Bean     public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,                            MessageListenerAdapter listenerAdapter) {      RedisMessageListenerContainer container = new RedisMessageListenerContainer();      container.setConnectionFactory(connectionFactory);      //订阅了一个叫redis.uncache的通道      container.addMessageListener(listenerAdapter, new PatternTopic("redis.uncache"));      //这个container 可以添加多个 messageListener      return container;     }     /**     * 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法     * @param receiver     * @return     */     @Bean     MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) {      //这个地方 是给messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“handle”      return new MessageListenerAdapter(receiver, "handle");     }   }

消息发布类如下:

  import com.xuanwu.apaas.core.multicache.CacheFactory;   import org.apache.commons.lang3.StringUtils;   import org.slf4j.Logger;   import org.slf4j.LoggerFactory;   import org.springframework.beans.factory.annotation.Autowired;   import org.springframework.stereotype.Component;   @Component   public class MessageSubscriber {     private static final Logger logger = LoggerFactory.getLogger(MessageSubscriber.class);     @Autowired     private CacheFactory cacheFactory;     /**      * 接收到redis订阅的消息后,将ehcache的缓存失效      * @param message 格式为name_key      */     public void handle(String message){       logger.debug("redis.ehcache:"+message);       if(StringUtils.isEmpty(message)) {         return;       }       String[] strs = message.split("#");       String name = strs[0];       String key = null;       if(strs.length == 2) {         key = strs[1];       }       cacheFactory.ehDel(name,key);     }   }

具体操作缓存的类如下:

  import com.xuanwu.apaas.core.multicache.publisher.MessagePublisher;   import net.sf.ehcache.Cache;   import net.sf.ehcache.CacheManager;   import net.sf.ehcache.Element;   import org.apache.commons.lang3.StringUtils;   import org.slf4j.Logger;   import org.slf4j.LoggerFactory;   import org.springframework.beans.factory.annotation.Autowired;   import org.springframework.data.redis.RedisConnectionFailureException;   import org.springframework.data.redis.core.HashOperations;   import org.springframework.data.redis.core.RedisTemplate;   import org.springframework.stereotype.Component;   import java.io.InputStream;   /**    * 多级缓存切面    * @author rongdi    */   @Component   public class CacheFactory {     private static final Logger logger = LoggerFactory.getLogger(CacheFactory.class);     @Autowired     private RedisTemplate redisTemplate;     @Autowired     private MessagePublisher messagePublisher;     private CacheManager cacheManager;     public CacheFactory() {       InputStream is = this.getClass().getResourceAsStream("/ehcache.xml");       if(is != null) {         cacheManager = CacheManager.create(is);       }     }     public void cacheDel(String name,String key) {       //删除redis对应的缓存       redisDel(name,key);       //删除本地的ehcache缓存,可以不需要,订阅器那里会删除      //  ehDel(name,key);       if(cacheManager != null) {         //发布一个消息,告诉订阅的服务该缓存失效         messagePublisher.publish(name, key);       }     }     public String ehGet(String name,String key) {       if(cacheManager == null) return null;       Cache cache=cacheManager.getCache(name);       if(cache == null) return null;       cache.acquireReadLockOnKey(key);       try {         Element ele = cache.get(key);         if(ele == null) return null;         return (String)ele.getObjectValue();       } finally {         cache.releaseReadLockOnKey(key);       }     }     public String redisGet(String name,String key) {       HashOperations<String,String,String> oper = redisTemplate.opsForHash();       try {         return oper.get(name, key);       } catch(RedisConnectionFailureException e) {         //连接失败,不抛错,直接不用redis缓存了         logger.error("connect redis error ",e);         return null;       }     }     public void ehPut(String name,String key,String value) {       if(cacheManager == null) return;       if(!cacheManager.cacheExists(name)) {         cacheManager.addCache(name);       }       Cache cache=cacheManager.getCache(name);       //获得key上的写锁,不同key互相不影响,类似于synchronized(key.intern()){}       cache.acquireWriteLockOnKey(key);       try {         cache.put(new Element(key, value));       } finally {         //释放写锁         cache.releaseWriteLockOnKey(key);       }     }     public void redisPut(String name,String key,String value) {       HashOperations<String,String,String> oper = redisTemplate.opsForHash();       try {         oper.put(name, key, value);       } catch (RedisConnectionFailureException e) {         //连接失败,不抛错,直接不用redis缓存了         logger.error("connect redis error ",e);       }     }     public void ehDel(String name,String key) {       if(cacheManager == null) return;       if(cacheManager.cacheExists(name)) {         //如果key为空,直接根据缓存名删除         if(StringUtils.isEmpty(key)) {           cacheManager.removeCache(name);         } else {           Cache cache=cacheManager.getCache(name);           cache.remove(key);         }       }     }     public void redisDel(String name,String key) {       HashOperations<String,String,String> oper = redisTemplate.opsForHash();       try {         //如果key为空,直接根据缓存名删除         if(StringUtils.isEmpty(key)) {           redisTemplate.delete(name);         } else {           oper.delete(name,key);         }       } catch (RedisConnectionFailureException e) {         //连接失败,不抛错,直接不用redis缓存了         logger.error("connect redis error ",e);       }     }   }

工具类如下

  import com.fasterxml.jackson.core.type.TypeReference;   import com.fasterxml.jackson.databind.DeserializationFeature;   import com.fasterxml.jackson.databind.JavaType;   import com.fasterxml.jackson.databind.ObjectMapper;   import org.apache.commons.lang3.StringUtils;   import org.json.JSONArray;   import org.json.JSONObject;   import java.util.*;   public class JsonUtil {     private static ObjectMapper mapper;     static {       mapper = new ObjectMapper();       mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,           false);     }          /**      * 将对象序列化成json      *      * @param obj 待序列化的对象      * @return      * @throws Exception      */     public static String serialize(Object obj) throws Exception {       if (obj == null) {         throw new IllegalArgumentException("obj should not be null");       }       return mapper.writeValueAsString(obj);     }     /**       带泛型的反序列化,比如一个JSONArray反序列化成List<User>     */     public static <T> T deserialize(String jsonStr, Class<?> collectionClass,                     Class<?>... elementClasses) throws Exception {       JavaType javaType = mapper.getTypeFactory().constructParametrizedType(           collectionClass, collectionClass, elementClasses);       return mapper.readValue(jsonStr, javaType);     }          /**      * 将json字符串反序列化成对象      * @param src 待反序列化的json字符串      * @param t  反序列化成为的对象的class类型      * @return      * @throws Exception      */     public static <T> T deserialize(String src, Class<T> t) throws Exception {       if (src == null) {         throw new IllegalArgumentException("src should not be null");       }       if("{}".equals(src.trim())) {         return null;       }       return mapper.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")

附上主要的依赖包

  1. "org.springframework.boot:spring-boot-starter-redis:1.4.2.RELEASE",

  2. 'net.sf.ehcache:ehcache:2.10.4',

  3. "org.json:json:20160810"

springboot是什么

springboot一种全新的编程规范,其设计目的是用来简化新Spring应用的初始搭建以及开发过程,SpringBoot也是一个服务于框架的框架,服务范围是简化配置文件。

以上就是怎么在springboot中自定义两级缓存,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI