欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

springboot中使用自定义两级缓存的方法

程序员文章站 2023-12-18 22:04:40
工作中用到了springboot的缓存,使用起来挺方便的,直接引入redis或者ehcache这些缓存依赖包和相关缓存的starter依赖包,然后在启动类中加入@enabl...

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: