缓存篇(二)- JetCache_jetcache cacherefresh-程序员宅基地

技术标签: JetCache  分布式锁  缓存  分布式缓存  雪崩效应  

本文将由浅入深,从基本特性介绍,从简单demo使用,到JetCache源码分析,到Spring Aop的源码分析,到如何利用这些知识去自己尝试写一个自己的cache小demo,去做一个全面的概括。

*背景和特性

*用法demo

*JetCache源码分析

*Spring Aop的支持和源码分析

*写一个简单的cache框架demo

 

背景和特性

对于一些cache框架或产品,我们可以发现一些明显不足。

Spring cache:无法满足本地缓存和远程缓存同时使用,使用远程缓存时无法自动刷新

Guava cache:内存型缓存,占用内存,无法做分布式缓存

redis/memcache:分布式缓存,缓存失效时,会导致数据库雪崩效应

Ehcache:内存型缓存,可以通过RMI做到全局分布缓存,效果差

基于以上的一些不足,大杀器缓存框架JetCache出现,基于已有缓存的成熟产品,解决了上面产品的缺陷。主要表现在

(1)分布式缓存和内存型缓存可以共存,当共存时,优先访问内存,保护远程缓存;也可以只用某一种,分布式 or 内存

(2)自动刷新策略,防止某个缓存失效,访问量突然增大时,所有机器都去访问数据库,可能导致数据库挂掉

(3)利用不严格的分布式锁,对同一key,全局只有一台机器自动刷新

 

用法demo

可查看代码:https://github.com/zhuzhenke/common-caches/tree/master/jetcache

项目环境SpringBoot + jdk1.8+jetcache2.5.7

SpringApplication的main类注解,这个是必须要加的,否则jetCache无法代理到含有对应注解的类和方案

@SpringBootApplication
@ComponentScan("com.cache.jetcache")
@EnableMethodCache(basePackages = "com.cache.jetcache")
@EnableCreateCacheAnnotation

 

resource下创建application.yml

jetcache:
  statIntervalMinutes: 1
  areaInCacheName: false
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson
  remote:
    default:
      type: redis
      keyConvertor: fastjson
      valueEncoder: java
      valueDecoder: java
      poolConfig:
        minIdle: 5
        maxIdle: 20
        maxTotal: 50
      host: 127.0.0.1
      port: 6379

现在用CategoryService为例,介绍简单的用法

@Service
public class CategoryService {

    @CacheInvalidate(name = CategoryCacheConstants.CATEGORY_DOMAIN,
            key = "#category.getCategoryCacheKey()")
    public int add(Category category) {
        System.out.println("模拟进行数据库交互操作......");
        System.out.println("Cache became invalid,value:" + CategoryCacheConstants.CATEGORY_DOMAIN
                + ",key:" + category.getCategoryCacheKey());
        return 1;
    }


    @CacheInvalidate(name = CategoryCacheConstants.CATEGORY_DOMAIN,
            key = "#category.getCategoryCacheKey()")
    public int delete(Category category) {
        System.out.println("模拟进行数据库交互操作......");
        System.out.println("Cache became invalid,value:" + CategoryCacheConstants.CATEGORY_DOMAIN
                + ",key:" + category.getCategoryCacheKey());
        return 0;
    }


    @CacheUpdate(name = CategoryCacheConstants.CATEGORY_DOMAIN,
            value = "#category",
            key = "#category.getCategoryCacheKey()")
    public int update(Category category) {
        System.out.println("模拟进行数据库交互操作......");
        System.out.println("Cache updated,value:" + CategoryCacheConstants.CATEGORY_DOMAIN
                + ",key:" + category.getCategoryCacheKey()
                + ",category:" + category);
        return 1;
    }


    @Cached(name = CategoryCacheConstants.CATEGORY_DOMAIN,
            expire = 3600,
            cacheType = CacheType.BOTH,
            key = "#category.getCategoryCacheKey()")
    @CacheRefresh(refresh = 60)
    public Category get(Category category) {
        System.out.println("模拟进行数据库交互操作......");
        Category result = new Category();
        result.setCateId(category.getCateId());
        result.setCateName(category.getCateId() + "JetCateName");
        result.setParentId(category.getCateId() - 10);
        return result;
    }
}

demo中的CategoryService可以直接用类或接口+类的方式来使用,这里在对应类中注入CategoryService,调用对应方法即可使用缓存,方便快捷。

关于其他用法,@CreateCache显式使用,类似Map的使用,支持异步获取等功能,自带缓存统计信息功能等功能这里不再过多解释。

常用注解说明

@Cached:将方法的结果缓存下来,可配置cacheType参数:REMOTE, LOCAL, BOTH,LOCAL时可配置localLimit参数来设置本地local缓存的数量限制。condition参数可配置在什么情况下使用缓存,condition和key支持SPEL语法

@CacheInvalidate:缓存失效,同样可配置condition满足的情况下失效缓存。不足:不能支持是在方法调用前还是调用后将缓存失效

@CacheUpdate:缓存更新,value为缓存更新后的值。此操作是调用原方法结束后将更新缓存

@CreateCache:用于字段上的注解,创建缓存。根据参数,创建一个name的缓存,可以全局显式使用这个缓存参数对象

@CacheRefresh:自动刷新策略,可设置refresh、stopRefreshAfterLastAccess、refreshLockTimeout参数。

注意点

JetCache也是基于Spring Aop来实现,当然就存在固有的不足。表现在当是同一个类中方法内部调用,则被调用方法的缓存策略不能生效。当然如果非要这么做,可以使用AopProxy.currentProxy().do()的方式去避免这样的问题,不过代码看起来就不是这么优美了。

适合场景

适合场景:

(1)对于更新不频繁,时效性不高,key的量不大但是访问量高的场景,如新闻网站的热点新闻,电商系统的商品信息(如标题,属性,商品详情等),微博热帖

 

不适合场景

(1)更新频繁,且对数据实时性要求很高,如电商系统的库存,商品价格

(2)key的量多,需要自动刷新的key量也多。内部实现JetCacheExecutor的heavyIOExecutor默认使用10个线程的线程池,也可以自行设置定制,但是容易受到单机的限制

 

JetCache源码分析

 

application.yml配置的生效

(1)spring.factories中配置了org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alicp.jetcache.autoconfigure.JetCacheAutoConfiguration,JetCacheAutoConfiguration中对GlobalCacheConfig进行了注入,globalCacheConfig()中的参数AutoConfigureBeans和JetCacheProperties类,说明在这之前Spring IOC已经对这个类进行了注入。

(2)在创建LinkedHashMapAutoConfiguration和RedisAutoConfiguration过程中,AbstractCacheAutoInit类@PostConstruct注解的init方法会被调用。init方法,则对application.yml的process方法,会分别对jetcache.local和jetcache.remote参数进行解析,并分别将解析后的数据创建成对应的CacheBuilder,存放在autoConfigureBeans的localCacheBuilders和remoteCacheBuilders属性中,其map对应的key为application.yml配置的default,这也说明可以配置多个

(3)CacheBuilder在version2.5.7及以前,仅支持CaffeineCacheBuilder、LinkedHashMapCacheBuilder和RedisCacheBuilder

 

注解生效

(1)JetCacheProxyConfiguration中注入了CacheAdvisor,CacheAdvisor绑定了CachePointcut和JetCacheInterceptor。这里的advisor类似我们常理解的Spring Aspect,只不过advisor是在集成Aspect之前的内部切面编程实现。不同的是advisor只支持一个PointCut和一个Advice,Aspect均可以支持多个。

(2)CachePointcut实现StaticMethodMatcherPointcut和集成ClassFilter,它的作用非常关键。在Spring IOC的createBean过程中,会去调用这里的matches方法,来对创建相应的类的代理类,只有matches方法在匹配上了注解时返回true时,Spring才会创建代理类,会根据对应目标类是否有接口来使用jdk或cglib创建代理类,这里用到了动态代理。

(3)那么注解在哪里生效呢?还是在CachePoint中,当matchesImpl(Method method, Class targetClass)会对方法的注解进行解析和配置保存,这里会调用到CacheConfigUtil的parse方法。

public static boolean parse(CacheInvokeConfig cac, Method method) {
        boolean hasAnnotation = false;
        CachedAnnoConfig cachedConfig = parseCached(method);
        if (cachedConfig != null) {
            cac.setCachedAnnoConfig(cachedConfig);
            hasAnnotation = true;
        }
        boolean enable = parseEnableCache(method);
        if (enable) {
            cac.setEnableCacheContext(true);
            hasAnnotation = true;
        }
        CacheInvalidateAnnoConfig invalidateAnnoConfig = parseCacheInvalidate(method);
        if (invalidateAnnoConfig != null) {
            cac.setInvalidateAnnoConfig(invalidateAnnoConfig);
            hasAnnotation = true;
        }
        CacheUpdateAnnoConfig updateAnnoConfig = parseCacheUpdate(method);
        if (updateAnnoConfig != null) {
            cac.setUpdateAnnoConfig(updateAnnoConfig);
            hasAnnotation = true;
        }

        if (cachedConfig != null && (invalidateAnnoConfig != null || updateAnnoConfig != null)) {
            throw new CacheConfigException("@Cached can't coexists with @CacheInvalidate or @CacheUpdate: " + method);
        }

        return hasAnnotation;
    }

这里会对几个常用的关键注解进行解析,这里我们没有看到@CacheRefresh注解的解析,@CacheRefresh的解析工作放在了parseCached方法中,同时也说明了缓存自动刷新功能是基于@Cached注解的,刷新任务是在调用带有@Cached方法时才会生效。

(4)方法缓存的配置会存放在CacheInvokeConfig类中

 

缓存生效

(1)上面有提到CacheAdvisor绑定了CachePointcut和JetCacheInterceptor,且已完成注解的配置生效。CachePointcut方法创建了代理类,作为JetCacheInterceptor会对代理类的方法进行拦截,来完成缓存的更新和失效等

(2)当调用含有jetcache的注解时,程序会走到JetCacheInterceptor.invoke()方法,继而走到CacheHandler.doInvoke()方法。

private static Object doInvoke(CacheInvokeContext context) throws Throwable {
        CacheInvokeConfig cic = context.getCacheInvokeConfig();
        CachedAnnoConfig cachedConfig = cic.getCachedAnnoConfig();
        if (cachedConfig != null && (cachedConfig.isEnabled() || CacheContextSupport._isEnabled())) {
            return invokeWithCached(context);
        } else if (cic.getInvalidateAnnoConfig() != null || cic.getUpdateAnnoConfig() != null) {
            return invokeWithInvalidateOrUpdate(context);
        } else {
            return invokeOrigin(context);
        }
    }

这里用到了CacheInvokeConfig保存的注解信息,调用时会根据当前方法的注解,@Cached的调用invokeWithCached()方法,@CacheUpdate和@CacheInvalidate的调用invokeWithInvalidateOrUpdate()方法。

(3)自动刷新功能。这里看下invokeWithCached()方法中有这么一段程序

Object result = cache.computeIfAbsent(key, loader);
            if (cache instanceof CacheHandlerRefreshCache) {
                // We invoke addOrUpdateRefreshTask manually
                // because the cache has no loader(GET method will not invoke it)
                ((CacheHandlerRefreshCache) cache).addOrUpdateRefreshTask(key, loader);
            }

这里在取得原方法的结果后,会保存到cache中,如果是cacheType是BOTH,则会各存一份。内存缓存是基于LRU原则的LinkedHashMap实现。这里在put缓存后,会对当前key进行一个addOrUpdateRefreshTask操作。这就是配置的@CacheRefresh注解发挥作用的地方。

protected void addOrUpdateRefreshTask(K key, CacheLoader<K,V> loader) {
        RefreshPolicy refreshPolicy = config.getRefreshPolicy();
        if (refreshPolicy == null) {
            return;
        }
        long refreshMillis = refreshPolicy.getRefreshMillis();
        if (refreshMillis > 0) {
            Object taskId = getTaskId(key);
            RefreshTask refreshTask = taskMap.computeIfAbsent(taskId, tid -> {
                logger.debug("add refresh task. interval={},  key={}", refreshMillis , key);
                RefreshTask task = new RefreshTask(taskId, key, loader);
                task.lastAccessTime = System.currentTimeMillis();
                ScheduledFuture<?> future = JetCacheExecutor.heavyIOExecutor().scheduleWithFixedDelay(
                        task, refreshMillis, refreshMillis, TimeUnit.MILLISECONDS);
                task.future = future;
                return task;
            });
            refreshTask.lastAccessTime = System.currentTimeMillis();
        }
    }

这里创建了一个RefreshTask(Runnable)类,并放入核心线程数为10的ScheduledThreadPoolExecutor,
ScheduledThreadPoolExecutor可根据实际情况自己定制。

public void run() {
            try {
                if (config.getRefreshPolicy() == null || (loader == null && !hasLoader())) {
                    cancel();
                    return;
                }
                long now = System.currentTimeMillis();
                long stopRefreshAfterLastAccessMillis = config.getRefreshPolicy().getStopRefreshAfterLastAccessMillis();
                if (stopRefreshAfterLastAccessMillis > 0) {
                    if (lastAccessTime + stopRefreshAfterLastAccessMillis < now) {
                        logger.debug("cancel refresh: {}", key);
                        cancel();
                        return;
                    }
                }
                logger.debug("refresh key: {}", key);
                Cache concreteCache = concreteCache();
                if (concreteCache instanceof AbstractExternalCache) {
                    externalLoad(concreteCache, now);
                } else {
                    load();
                }
            } catch (Throwable e) {
                logger.error("refresh error: key=" + key, e);
            }
        }

RefreshTask会对设置了stopRefreshAfterLastAccessMillis,且超过stopRefreshAfterLastAccessMillis时间未访问的RefreshTask任务进行取消。自动刷新功能是利用反射对原方法进行调用,并将结果缓存到对应的缓存中。这里需要说明一下,如果cacheType为BOTH时,只会对远程缓存进行刷新。

(4)分布式锁。分布式缓存自动刷新必定有多台机器都可能有相同的任务,那么每台机器都可能在同一时间刷新缓存必然是浪费,但是jetcache是没有一个全局任务分配的功能的。这里jetcache也非常聪明,利用了一个非严格的分布式锁,只有获取了这个key的分布式锁,才可以进行这个key的缓存刷新。分布式锁是向远程缓存写入一个lockKey为name+name+key+"_#RL#",value为uuid的缓存,写入成功则获取分布式锁成功。

(5)避免滥用@CacheRefresh注解。 @CacheRefresh注解其实就是解决雪崩效应的,但是我们不能滥用,否则非常不可控。

这里我们也看到了,后台刷新任务是针对单个key的,每个key对应一个Runnable,对系统的线程池是一个考验,所以不能过度依赖自动刷新。我们需要保证key是热点且数量有限的,否则每个机器都会保存一个key对应的Runnable是比较危险的事情。这里可以活用condition的选项,在哪些情况下使用自动刷新功能。比如微博热帖,我们可以根据返回的微博贴的阅读数,超过某个值之后,将这个热帖加入到自动刷新任务中。

 

Spring Aop的支持和源码分析

由于篇幅原因,这里的源码分析将不会做过多的分析。后续将利用单独的篇幅来分析。这里给出几个IOC和Aop比较关键的几个类和方法,可以参考并debug来阅读源码。可以按照这个顺序来看Spring的相关源码

DefaultListableBeanFactory.preInstantiateSingletons()

AbstractBeanFactory.getBean()

AbstractBeanFactory.doGetBean()

DefaultSingletonBeanRegistry.getSingleton()

AbstractBeanFactory.doGetBean()

AbstractAutowireCapableBeanFactory.createBean()

AbstractAutowireCapableBeanFactory.doCreateBean()

AbstractAutowireCapableBeanFactory.initializeBean()

AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization()

AbstractAutoProxyCreator.postProcessAfterInitialization()

AbstractAutoProxyCreator.wrapIfNecessary(),jdk/cglib代理的创建就是在这个方法的。

AbstractAdvisorAutoProxyCreator.findAdvisorsThatCanApply()

AopUtils.findAdvisorsThatCanApply()

AopUtils.canApply()

 

 

写一个简单的cache框架demo

首先我们看jetcache的源码,是去理解他的核心思路和原理去的。分析下来jetcache并没想象中那么难,难的只是细节和完善。如果对于jetcache有自己觉得不够友好的地方,理解过后完全可以自己改进。

如果理解了jetcache的大致原理,相信可以把这种思想思路用到很多其他的方面。

 

结束语

如果有写错的地方,欢迎大家提出。如果对上面的理解有问题,请留言,看到后必定及时回复解答。

本文为原创文章,码字不易,谢谢大家支持。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/codingtu/article/details/82888891

智能推荐

十进制小数部分如何转化成二进制算法实现_小数进制转换编程-程序员宅基地

文章浏览阅读483次。十进制小数转化成二进制,就是不断地乘二,判断之后的这个数是否比1大,比1大则输出输出1,留下小数部分继续前面的操作。将3.75的小数部分转化为二进制。最后的输出结果是.11。_小数进制转换编程

免费OFD文件在线转PDF_在线生成一个 ofd文件-程序员宅基地

文章浏览阅读739次。ofd文件打不开?ofd怎么转换为pdf?本文将给告诉大家ofd是什么文件格式?ofd怎么打开?ofd怎么免费转换为配pdf文件等,以下是具体的方法:一,什么是OFD文件?OFD是我国电子公文交换和存储格式标准。OFD格式是我国自主可控的电子文件版式文档格式。OFD版式文件,版面固定、不跑版、所见即所得,可以视为计算机时代的“数字纸张”;是电子文档发布、数字化信息传播和存档的理想文档格式。OFD格式是当下对于全国产环境具有明显的优势。因此,在自主可控档案系统中,OFD格式无疑是自主可控档案系.._在线生成一个 ofd文件

14、HDFS 透明加密KMS_mapreduce读写sequencefile、mapfile、orcfile和parquetfil-程序员宅基地

文章浏览阅读3w次。HDFS中的数据会以block的形式保存在各台数据节点的本地磁盘中,但这些block都是明文的。通过Web UI页面找到Block的ID和副本位于的机器信息如果在操作系统中直接访问block所在的目录,通过Linux的cat命令是可以直接查看里面的内容的,且是明文。在datanode找到其文件为:HDFS透明加密(Transparent Encryption)支持端到端的透明加密,启用以后,对于一些需要加密的HDFS目录里的文件可以实现透明的加密和解密,而不需要修改用户的业务代码。_mapreduce读写sequencefile、mapfile、orcfile和parquetfile文件

Java SE 第三章 常用类 API_java se api常用类-程序员宅基地

文章浏览阅读92次。3.0 API概述https://www.oracle.com/cn/java/technologies/java-se-api-doc.html如何使用API看类的描述​ Random类是用于生成随机数的类看构造方法​ Random():无参构造方法 Random r = new Random();看成员方法​ public int nextInt(int n):产生的是一个[0,n)范围内的随机数调用方法: int number = r.nextInt(10_java se api常用类

3-16心电图多分类预测task01_机器学习在呼吸心跳信号检测中应用ti-程序员宅基地

文章浏览阅读285次。3-16心电图多分类预测task01一、赛题理解1.赛题理解1.数据概况1.评价指标二、baseline学习1.引入库2.读入数据3.数据预处理4.训练、测试数据准备5.模型训练总结提示:以下是本篇文章正文内容,下面案例可供参考一、赛题理解1.赛题理解以心电图心跳信号数据为背景,要求根据心电图感应数据预测心跳信号所属类别,其中心跳信号对应正常病例以及受不同心律不齐和心肌梗塞影响的病例,这是一个多分类的问题。1.数据概况以预测心电图心跳信号类别为任务,总数据量超过20万,主要为1列心跳信号序列数_机器学习在呼吸心跳信号检测中应用ti

【超好懂的比赛题解】第 45 届国际大学生程序设计竞赛(ICPC)亚洲区域赛(济南)_icpc国际大学生程序设计竞赛题目-程序员宅基地

文章浏览阅读930次,点赞2次,收藏2次。title : 第 45 届国际大学生程序设计竞赛(ICPC)亚洲区域赛(济南)tags : ACM,题解,练习记录。_icpc国际大学生程序设计竞赛题目

随便推点

工具系列:TensorFlow决策森林_(3)使用dtreeviz可视化-程序员宅基地

文章浏览阅读1.2k次,点赞19次,收藏19次。之前的教程演示了如何使用TensorFlow的决策森林(随机森林、梯度提升树和CART)分类器和回归器来准备数据、训练和评估。(我们将TensorFlow决策森林缩写为TF-DF。)您还学会了如何使用内置的函数可视化树,并显示特征重要性度量。本教程的目标是通过可视化更深入地解释分类器和回归器决策树。我们将查看详细的树结构图示,以及决策树如何划分特征空间以做出决策的描绘。树结构图帮助我们理解模型的行为,特征空间图帮助我们通过展示特征和目标变量之间的关系来理解数据。我们将使用的可视化库称为dtreeviz。_dtreeviz

MySQL8.0学习记录10 - 字符集与校对规则_mysql8.0存储系统元数据的字符集是-程序员宅基地

文章浏览阅读2.1k次。MySQL8.0字符集_mysql8.0存储系统元数据的字符集是

漫威所有电影的 按时间线的观影顺序-程序员宅基地

文章浏览阅读3.1k次。美国队长1 - 2011年惊奇队长 - 2019年钢铁侠1 - 2008年无敌浩克 - 2008年钢铁侠2 - 2010年雷神 - 2011年复仇者联盟 - 2012年雷神2 - 2013年钢铁侠3 - 2013年美国队长2 - 2014年复仇者联盟2 - 2015年银河护卫队 - 2017年蚁人 - 2015年美国队长3 - 2016年奇异博士 - 2016年银河护卫队2 - 2017..._漫威电影观看顺序时间线

PhotoZoom Classic 7中的新功能-程序员宅基地

文章浏览阅读142次。众所周知PhotoZoom Classic是家庭使用理想的放大图像软件。目前很多用户还在使用PhotoZoom Classic 6,对于PhotoZoom Classic 7还是有点陌生。其实在6代衍生下出了7代,7代比6代多了很多适用的功能。下面我们就介绍一下PhotoZoom Classic 7中的新功能。PhotoZoom Classic 6的功能我们就不过多介绍,主要介绍7代中特有的功..._photozoon的作用

tensorflow中tf.keras.models.Sequential()用法-程序员宅基地

文章浏览阅读4.6w次,点赞75次,收藏349次。tensorflow中tf.keras.models.Sequential()用法Sequential()方法是一个容器,描述了神经网络的网络结构,在Sequential()的输入参数中描述从输入层到输出层的网络结构model = tf.keras.models.Sequential([网络结构]) #描述各层网络网络结构举例:拉直层:tf.keras.layers.Flatten() #拉直层可以变换张量的尺寸,把输入特征拉直为一维数组,是不含计算参数的层全连接层:tf.ker._tf.keras.models.sequential

Java递归实现Fibonacci数列计算_用递归方法编程计算fibonacci数列:(n=10),fac.jpg-程序员宅基地

文章浏览阅读2.8k次。实现代码如下:public static int factorial(int n){ if (n <= 1){ return 1; } return factorial(n-1) + factorial(n-2); }测试代码如下:System.out.println(factorial(40));测..._用递归方法编程计算fibonacci数列:(n=10),fac.jpg

推荐文章

热门文章

相关标签