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

 

结束语

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

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


版权声明:本文为codingtu原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。