本文将由浅入深,从基本特性介绍,从简单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: 1areaInCacheName: falselocal:default:type: linkedhashmapkeyConvertor: fastjsonremote:default:type: rediskeyConvertor: fastjsonvalueEncoder: javavalueDecoder: javapoolConfig:minIdle: 5maxIdle: 20maxTotal: 50host: 127.0.0.1port: 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的大致原理,相信可以把这种思想思路用到很多其他的方面。

结束语

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

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

缓存篇(二)- JetCache相关推荐

  1. Spring Boot 揭秘与实战(二) 数据缓存篇 - EhCache

    文章目录 1. EhCache 集成 2. 源代码 本文,讲解 Spring Boot 如何集成 EhCache,实现缓存. 在阅读「Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门 ...

  2. Spring Boot 揭秘与实战(二) 数据缓存篇 - Guava Cache

    本文,讲解 Spring Boot 如何集成 Guava Cache,实现缓存. 博客地址:blog.720ui.com/ 在阅读「Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门」 ...

  3. 那些年我们一起追过的缓存写法(二)

    感谢园子里的同学对上一篇的支持,很高兴楼主的一些经验及想法能够对大家有一些帮助. 上次主要讨论缓存读写这块各种代码实现,本篇就上次的问题继续来,看看那些年折腾过的各种缓存做法. 阅读目录: 缓存预热 ...

  4. 在java中重写方法应遵循规则的包括_Java面试题集合篇二

    Java面试题之Java集合篇二1.遍历一个List有哪些不同的方式? List<String> strList = new ArrayList<>(); //使用for-ea ...

  5. 一篇文章带你熟悉 TCP/IP 协议(网络协议篇二)

    涤生_Woo 2017年11月11日阅读 15544 关注 一篇文章带你熟悉 TCP/IP 协议(网络协议篇二) 同样的,本文篇幅也比较长,先来一张思维导图,带大家过一遍. 一图看完本文 一. 计算机 ...

  6. 【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇二:基于OneNote难点突破和批量识别...

    篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblog ...

  7. 【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇二:基于OneNote难点突破和批量识别

    篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblog ...

  8. linux查看nas剩余大小,老徐玩NAS 篇二:我的群晖储存空间哪儿去了——100%破案的教程...

    老徐玩NAS 篇二:我的群晖储存空间哪儿去了--100%破案的教程 2019-05-26 23:28:21 74点赞 866收藏 36评论 前言 为了更好的体验Nas的功能,我前段时间终于安耐不住在J ...

  9. 显卡3d渲染测试软件,测评荟 篇二:3D建模和3D渲染是吃CPU还是显卡?以及专业图形显卡和游戏显卡的区别...

    测评荟 篇二:3D建模和3D渲染是吃CPU还是显卡?以及专业图形显卡和游戏显卡的区别 2019-09-11 13:20:52 9点赞 32收藏 8评论 编辑:测评荟 首先,专业图形显卡和游戏显卡区别 ...

  10. 服务器机箱 改造 桌面,捡垃圾攒台个人服务器 篇二:另类服务器机箱,废旧机箱壳改装...

    捡垃圾攒台个人服务器 篇二:另类服务器机箱,废旧机箱壳改装 2020-10-30 19:41:07 42点赞 110收藏 29评论 另类All in One服务器机箱选择思路,废旧机箱壳. 长久以来, ...

最新文章

  1. java编程思想(第四版)第二章练习题-12
  2. 【JavaScriptjQuery】返回顶部
  3. iOS项目架构 小谈
  4. 西安科技大学计算机学院保研,独臂姑娘,好样的!
  5. docker $PWD路径_Docker 数据持久化
  6. 《黑客秘笈——渗透测试实用指南(第2版)》—第1章1.6学习
  7. 浅谈Java内存模型——JVM
  8. 神坛上的插画师真的高薪且自由吗?
  9. C# 小数点后保留两位小数,四舍五入的函数及使用方法
  10. java反射机制和运用
  11. ICX285 ICX205 ICX414 3CCD共用驱动板电路设计
  12. Android直播开发之旅(14):使用RC4算法加解密音视频流
  13. directadmin php-fpm,centos7.5最新内核安装DirectAdmin面板
  14. 【黑马程序员】vue学习笔记(未完)
  15. 男神体 骚包体 快乐体 手拙体 好身体(haha),你知道这些字体的英文名字吗?
  16. Java导出Excel加电子回章_使用Excel制作一个喜欢的DIY电子印章
  17. 将英文kali改成中文kali
  18. react中表格的数据渲染
  19. SINAMICS DCM 功率单元的动态过载能力
  20. Devc++设置代码自动补全\提示

热门文章

  1. [转]部分日文实用网址介绍
  2. 腾讯云Elasticsearch集群多可用区容灾实现原理及最佳实践
  3. 数据库实验一:数据库与数据表定义(2)—— 数据表相关操作
  4. substring从指定字符串开始截取
  5. Linux shell 交互式编程、TCL/TK 和 Expect 编译与安装、expect 编程
  6. OceanBase发布“珊瑚计划”,让合作伙伴成为OceanBase成功的关键
  7. SSH登录及失败解决
  8. 第八周编程题在线测试
  9. java 废弃方法_在Java中使用Deprecated方法或类是错误的吗?
  10. SmartUpload综合