App下載

詳解springboot中自定義兩級緩存的方法

陳情匿舊酒 2021-08-19 14:37:20 瀏覽數(shù) (2396)
反饋

我們在使用springboot的時候,總是需要去訪問數(shù)據(jù)庫,如果過于頻繁的訪問,會給數(shù)據(jù)庫造成很大的壓力。這期間添加緩存,就是為了減輕了springboot訪問數(shù)據(jù)庫時的壓力。下面,為大家講講springboot中是如何自定義兩級緩存。

       工作中用到了springboot的緩存,使用起來挺方便的,直接引入redis或者ehcache這些緩存依賴包和相關緩存的starter依賴包,然后在啟動類中加入@EnableCaching注解,然后在需要的地方就可以使用@Cacheable和@CacheEvict使用和刪除緩存了。這個使用很簡單,相信用過springboot緩存的都會玩,這里就不再多說了。美中不足的是,springboot使用了插件式的集成方式,雖然用起來很方便,但是當你集成ehcache的時候就是用ehcache,集成redis的時候就是用redis。如果想兩者一起用,ehcache作為本地一級緩存,redis作為集成式的二級緩存,使用默認的方式據(jù)我所知是沒法實現(xiàn)的(如果有高人可以實現(xiàn),麻煩指點下我)。畢竟很多服務需要多點部署,如果單獨選擇ehcache可以很好地實現(xiàn)本地緩存,但是如果在多機之間共享緩存又需要比較費時的折騰,如果選用集中式的redis緩存,因為每次取數(shù)據(jù)都要走網(wǎng)絡,總感覺性能不會太好。

  為了不要侵入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中應該是出現(xiàn)cache1 -> map(key1,value1)這種形式,其中cache1作為緩存名稱,map作為緩存的值,key作為map里的鍵,可以有效的隔離不同的緩存名稱下的緩存。但是實際上redis里確是cache1 -> 空(zset)和key1 -> value1,兩個獨立的鍵值對,試驗得知不同的緩存名稱下的緩存完全是共用的,如果有感興趣的朋友可以去試驗下,也就是說這個value屬性實際上是個擺設,鍵的唯一性只由key屬性保證。我只能認為這是spring的緩存實現(xiàn)的bug,或者是特意這么設計的,(如果有知道啥原因的歡迎指點)。

  回到正題,有了注解還需要有個注解處理類,這里我使用aop的切面來進行攔截處理,原生的實現(xiàn)其實也大同小異。切面處理類如下:

    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;

        //這里通過一個容器初始化監(jiān)聽器,根據(jù)外部配置的@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) {

            //得到被切面修飾的方法的參數(shù)列表
            Object[] args = joinPoint.getArgs();
            // result是方法的最終返回結果
            Object result = null;
            //如果沒有開啟緩存,直接調(diào)用處理方法返回
            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);
            //獲得經(jīng)過el解析后的key值
            String key = parseKey(ca.key(),method,args);
            Class<?> elementClass = ca.type();
            //從注解中獲取緩存名稱
            String name = ca.value();

            try {
                //先從ehcache中取數(shù)據(jù)
                String cacheValue = cacheFactory.ehGet(name,key);
                if(StringUtils.isEmpty(cacheValue)) {
                    //如果ehcache中沒數(shù)據(jù),從redis中取數(shù)據(jù)
                    cacheValue = cacheFactory.redisGet(name,key);
                    if(StringUtils.isEmpty(cacheValue)) {
                        //如果redis中沒有數(shù)據(jù)
                        // 調(diào)用業(yè)務方法得到結果
                        result = joinPoint.proceed(args);
                        //將結果序列化后放入redis
                        cacheFactory.redisPut(name,key,serialize(result));
                    } else {
                        //如果redis中可以取到數(shù)據(jù)
                        //將緩存中獲取到的數(shù)據(jù)反序列化后返回
                        if(elementClass == Exception.class) {
                            result = deserialize(cacheValue, returnType);
                        } else {
                            result = deserialize(cacheValue, returnType,elementClass);
                        }
                    }
                    //將結果序列化后放入ehcache
                    cacheFactory.ehPut(name,key,serialize(result));
                } else {
                    //將緩存中獲取到的數(shù)據(jù)反序列化后返回
                    if(elementClass == Exception.class) {
                        result = deserialize(cacheValue, returnType);
                    } else {
                        result = deserialize(cacheValue, returnType,elementClass);
                    }
                }

            } catch (Throwable throwable) {
                logger.error("",throwable);
            }

           return result;
        }

        /**
         * 在方法調(diào)用前清除緩存,然后調(diào)用業(yè)務方法
         * @param joinPoint
         * @return
         * @throws Throwable
         *
         */
        @Around("cacheEvict()")
        public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable {
            // 得到被代理的方法
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            //得到被切面修飾的方法的參數(shù)列表
            Object[] args = joinPoint.getArgs();
            // 得到被代理的方法上的注解
            CacheEvict ce = method.getAnnotation(CacheEvict.class);
            //獲得經(jīng)過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;

            //獲取被攔截方法參數(shù)名列表(使用Spring支持類庫)
            LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
            String[] paraNameArr = u.getParameterNames(method);

            //使用SPEL進行key的解析
            ExpressionParser parser = new SpelExpressionParser();
            //SPEL上下文
            StandardEvaluationContext context = new StandardEvaluationContext();
            //把方法參數(shù)放入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變量控制是否使用緩存,為了實現(xiàn)無縫的接入springboot,必然需要受到原生@EnableCaching注解的控制,這里我使用一個spring容器加載完成的監(jiān)聽器,然后在監(jiān)聽器里找到是否有被@EnableCaching注解修飾的類,如果有就從spring容器拿到MultiCacheAspect對象,然后將cacheEnable設置成true。這樣就可以實現(xiàn)無縫接入springboot,不知道朋友們還有沒有更加優(yōu)雅的方法呢?歡迎交流!監(jiān)聽器類如下

    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容器,防止出現(xiàn)調(diào)用兩次的情況(mvc加載也會觸發(fā)一次)
            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);
                }

            }
        }  
    }

  實現(xiàn)了無縫接入,還需要考慮多點部署的時候,多點的ehcache怎么和redis緩存保持一致的問題。在正常應用中,一般redis適合長時間的集中式緩存,ehcache適合短時間的本地緩存,假設現(xiàn)在有A,B和C服務器,A和B部署了業(yè)務服務,C部署了redis服務。當請求進來,前端入口不管是用LVS或者nginx等負載軟件,請求都會轉發(fā)到某一個具體服務器,假設轉發(fā)到了A服務器,修改了某個內(nèi)容,而這個內(nèi)容在redis和ehcache中都有,這時候,A服務器的ehcache緩存,和C服務器的redis不管控制緩存失效也好,刪除也好,都比較容易,但是這時候B服務器的ehcache怎么控制失效或者刪除呢?一般比較常用的方式就是使用發(fā)布訂閱模式,當需要刪除緩存的時候在一個固定的通道發(fā)布一個消息,然后每個業(yè)務服務器訂閱這個通道,收到消息后刪除或者過期本地的ehcache緩存(最好是使用過期,但是redis目前只支持對key的過期操作,沒辦法操作key下的map里的成員的過期,如果非要強求用過期,可以自己加時間戳自己實現(xiàn),不過用刪除出問題的幾率也很小,畢竟加緩存的都是讀多寫少的應用,這里為了方便都是直接刪除緩存)。總結起來流程就是更新某條數(shù)據(jù),先刪除redis中對應的緩存,然后發(fā)布一個緩存失效的消息在redis的某個通道中,本地的業(yè)務服務去訂閱這個通道的消息,當業(yè)務服務收到這個消息后去刪除本地對應的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;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class MultiCacheConfig {

   @Bean
   public CacheManager cacheManager(RedisTemplate redisTemplate) {
      RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
      //設置緩存過期時間(秒)
      Map<String, Long> expires = new HashMap<>();
      expires.put("ExpOpState",0L);
      expires.put("ImpOpState",0L);
      rcm.setExpires(expires);
      rcm.setDefaultExpiration(600);
      return rcm;
   }

   @Bean
   public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
      StringRedisTemplate template = new StringRedisTemplate(factory);
      StringRedisSerializer redisSerializer = new StringRedisSerializer();
      template.setValueSerializer(redisSerializer);
      template.afterPropertiesSet();
      return template;
   }

   /**
    * redis消息監(jiān)聽器容器
    * 可以添加多個監(jiān)聽不同話題的redis監(jiān)聽器,只需要把消息監(jiān)聽器和相應的消息訂閱處理器綁定,該消息監(jiān)聽器
    * 通過反射技術調(diào)用消息訂閱處理器的相關方法進行一些業(yè)務處理
    * @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;
   }

   /**
    * 消息監(jiān)聽器適配器,綁定消息處理器,利用反射技術調(diào)用消息處理器的業(yè)務方法
    * @param receiver
    * @return
    */
   @Bean
   MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) {
      //這個地方 是給messageListenerAdapter 傳入一個消息接受的處理器,利用反射的方法調(diào)用“handle”
      return new MessageListenerAdapter(receiver, "handle");
   }

}

       消息發(fā)布類如下:

    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) {
            //發(fā)布一個消息,告訴訂閱的服務該緩存失效
            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;
        Cache cache = cacheManager.getCache(name);
        if(cache != null) {
            //如果key為空,直接根據(jù)緩存名刪除
            if(StringUtils.isEmpty(key)) {
                cacheManager.removeCache(name);
            } else {
                cache.remove(key);
            }
        }
    }

    public void redisDel(String name,String key) {
        HashOperations<String,String,String> oper = redisTemplate.opsForHash();
        try {
            //如果key為空,直接根據(jù)緩存名刪除
            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")

附上主要的依賴包

"org.springframework.boot:spring-boot-starter-redis:1.4.2.RELEASE",
'net.sf.ehcache:ehcache:2.10.4',
"org.json:json:20160810"

以上就是springboot中如何使用自定義兩級緩存的詳細內(nèi)容,更多關于springboot 使用緩存的資料,推薦大家閱讀Springboot的相關教程


0 人點贊