博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
缓存篇(二)- JetCache
阅读量:2443 次
发布时间:2019-05-10

本文共 11949 字,大约阅读时间需要 39 分钟。

本文将由浅入深,从基本特性介绍,从简单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

可查看代码:

项目环境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为例,介绍简单的用法

@Servicepublic 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
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的大致原理,相信可以把这种思想思路用到很多其他的方面。

 

结束语

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

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

转载地址:http://mqnqb.baihongyu.com/

你可能感兴趣的文章
colspan(HTML属性)
查看>>
sass导入sass_8条提示,帮助您充分利用Sass
查看>>
wordpress主题_所以您认为自己知道如何编写WordPress主题?
查看>>
vue 表格时间格式化_表格格式
查看>>
css -moz_moz-border-radius(CSS属性)
查看>>
跨度(HTML元素)
查看>>
wordpress当发布器_当我们与专家讨论WordPress时发生了什么
查看>>
css后代选择器与子选择器_后代选择器(CSS选择器)
查看>>
ppt修复演示文稿_使用WImpress建立惊人的演示文稿
查看>>
如何以正确的方式在您的WordPress网站上安装jQuery Mobile
查看>>
css选择器除了第一个_:第一个孩子(CSS选择器)
查看>>
html中的href属性_href(HTML属性)
查看>>
css3超链接文本样式_CSS3:文本样式和其他基础
查看>>
wordpress入门主题_WordPress儿童主题入门
查看>>
WordPress 3.5的新功能
查看>>
wordpress插件开发_使用免费WordPress插件的开发人员指南
查看>>
wordpress模板_如何在15分钟内定制WordPress模板
查看>>
wordpress插件开发_WordPress插件开发人员的10个必知技能
查看>>
joomla数据库表结构_WordPress v Joomla:简介和内容结构
查看>>
wordpress插件_在五分钟或更短的时间内设计一个WordPress插件
查看>>