麻豆小视频在线观看_中文黄色一级片_久久久成人精品_成片免费观看视频大全_午夜精品久久久久久久99热浪潮_成人一区二区三区四区

首頁 > 開發 > Java > 正文

springboot中使用自定義兩級緩存的方法

2024-07-14 08:41:00
字體:
來源:轉載
供稿:網友

工作中用到了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 @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"

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JAVA教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 成人午夜亚洲 | 国产无限资源在线观看 | 久久久毛片视频 | 31freehdxxxx欧美 | h视频在线免费观看 | 鲁久久| 日本成人一区 | 国产欧美在线观看不卡一 | h视频在线观看免费 | 亚洲乱码精品久久久久 | 欧美一级爱操视频 | 一本一本久久a久久精品综合小说 | 一级成人黄色片 | 嗯~啊~用力~高h | 77成人影院| 91成人久久| 欧美成年人视频在线观看 | 亚洲第一色婷婷 | 久久久久久久99 | av在线试看 | 毛片在线免费视频 | 无遮挡一级毛片视频 | 亚洲一区二区在线 | 国产一级毛片高清视频完整版 | 日本不卡一区二区在线观看 | 亚洲一二区精品 | 毛片在线免费 | 久久久久久久久久综合 | 黄色免费大片 | 啊~用cao嗯力cao烂我视频 | 91精品国产99久久久久久红楼 | 草莓视频久久 | 海外中文字幕在线观看 | 国产成人强伦免费视频网站 | 亚洲第一成av人网站懂色 | 国产成人免费精品 | 国产成人精品区一区二区不卡 | 成人黄视频在线观看 | 中文字幕在线视频日本 | xxxx69hd一hd | 国产免费高清在线视频 |