快取篇(二)- 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的大致原理,相信可以把這種思想思路用到很多其他的方面。
結束語
如果有寫錯的地方,歡迎大家提出。如果對上面的理解有問題,請留言,看到後必定及時回覆解答。
本文為原創文章,碼字不易,謝謝大家支援。