本文将由浅入深,从基本特性介绍,从简单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,全局只有一台机器自动刷新
可查看代码: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个线程的线程池,也可以自行设置定制,但是容易受到单机的限制
(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的选项,在哪些情况下使用自动刷新功能。比如微博热帖,我们可以根据返回的微博贴的阅读数,超过某个值之后,将这个热帖加入到自动刷新任务中。
由于篇幅原因,这里的源码分析将不会做过多的分析。后续将利用单独的篇幅来分析。这里给出几个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()
首先我们看jetcache的源码,是去理解他的核心思路和原理去的。分析下来jetcache并没想象中那么难,难的只是细节和完善。如果对于jetcache有自己觉得不够友好的地方,理解过后完全可以自己改进。
如果理解了jetcache的大致原理,相信可以把这种思想思路用到很多其他的方面。
如果有写错的地方,欢迎大家提出。如果对上面的理解有问题,请留言,看到后必定及时回复解答。
本文为原创文章,码字不易,谢谢大家支持。