1. 程式人生 > >快取篇(二)- JetCache

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

 

結束語

如果有寫錯的地方,歡迎大家提出。如果對上面的理解有問題,請留言,看到後必定及時回覆解答。

本文為原創文章,碼字不易,謝謝大家支援。