場景
統(tǒng)計一個批量接口會有多少數(shù)據(jù)涉枫,這個接口的QPS在100萬級別孽鸡。有幾種方案:
- 每次調(diào)用都串行計算一次哪轿;
- 每次調(diào)用使用線程池并行計算盈魁。
由于并發(fā)量特別的大,第1種場景肯定不適合窃诉,這會把相應(yīng)時間拉長杨耙。第二種方法每次請求過來都放到一個線程池里面請求,比第一種強很多飘痛,用這種方式基本上可以解決80%左右的需求了珊膜。那么還有能優(yōu)化的地方么?答案是有的宣脉。
Cache + 線程池
一般在大的公司都有一些監(jiān)控系統(tǒng)车柠,可以將監(jiān)控的數(shù)據(jù)上報到監(jiān)控系統(tǒng)中。上面兩個場景都是每次請求都會調(diào)用上報接口塑猖,這樣特別浪費資源也可能出現(xiàn)性能問題竹祷。是否可以想一個辦法減少上報次數(shù)呢?我們可以使用cache匯總在一起羊苟,打包通過線程池異步上報塑陵。是不是這種方式會更好一些。
實現(xiàn)
怎么實現(xiàn)呢蜡励? 首先我們需要一個cache令花,這次我們使用Guava Cache。
Guava Cache 是google開發(fā)開源項目Guava中帶有的功能凉倚,只提供堆緩存兼都,也就是說重啟機器后就沒有了,特點:小巧玲瓏稽寒,性能最好扮碧。
private volatile static Cache<String, MutableInt> metricCache = null;
public static Cache<String, MutableInt> getMetricCache(){
if (metricCache == null) {
synchronized (this) {
if (metricCache == null) {
metricCache = initMetricCache();
return metricCache;
}
}
}
return metricCache;
}
private static Cache<String, MutableInt> initMetricCache(){
Cache<String, MutableInt> initMetricCache = CacheBuilder.newBuilder()
// 設(shè)置緩存?zhèn)€數(shù)
.maximumSize(1024)
// 設(shè)置cache中的數(shù)據(jù)在寫入之后的存活時間為1秒
.expireAfterWrite(1, TimeUnit.MINUTES)
// 設(shè)置并發(fā)數(shù)為8,即同一時間最多只能有5個線程往cache執(zhí)行寫入操作
.concurrencyLevel(8)
// 聲明一個監(jiān)聽器瓦胎,緩存項被移除時做一些額外操作芬萍。這里使用異步線程池的形式實現(xiàn),更加高效搔啊。
.removalListener(RemovalListeners.asynchronous(new RemovalListener<String, MutableInt>(){
@Override
public void onRemoval(RemovalNotification<String, MutableInt> notification) {
// 刪除后的邏輯操作柬祠,這里是上報到監(jiān)控系統(tǒng)中
metricForCount(notification.getKey(), notification.getValue().intValue());
}
},
// 自定義線程池,這里就不在把實現(xiàn)的代碼粘進來了
taskExecutor.getTaskExecutor()))
.build();
return initMetricCache;
}
對上面的代碼進行分析:
-
CacheBuilder.newBuilder()
創(chuàng)建一個Guava Cache负芋,設(shè)置一些配置; - 在調(diào)用時考慮到高效性漫蛔,使用了一個小技巧延遲加載嗜愈,參考
getMetricCache()
實現(xiàn); - 在Guava Cache中使用
removalListener
特性莽龟,結(jié)合我們的需求蠕嫁,當統(tǒng)計記錄達到一定的數(shù)量后,刪除掉并在監(jiān)聽的線程池中實現(xiàn)上報毯盈。
應(yīng)用
看著很牛B剃毒,怎么使用呢?
public static void logMetricForCount(final String key, final int count) {
try {
MutableInt logMetric = getMetricCache().get(key, new Callable<MutableInt>() {
@Override
public MutableInt call() throws Exception {
return new MutableInt(0);
}
});
// 計數(shù)
logMetric.add(count);
if(logMetric.intValue() > 500){
// 當計數(shù)達到500個時刪除此key搂赋,從而觸發(fā)上面配置的removalListener
getMetricCache().invalidate(key);
}
} catch (Exception e) {
logger.warn("統(tǒng)計{}信息次數(shù){}異常", key, count, e);
}
}
在實戰(zhàn)的計數(shù)操作赘阀,apache提供了MutableInt專門用于高效計數(shù)的類。還使用到Guava Cache的特性脑奠。
MutableInt logMetric = getMetricCache().get(key, new Callable<MutableInt>() {
@Override
public MutableInt call() throws Exception {
return new MutableInt(0);
}
});
當沒有g(shù)et到數(shù)據(jù)時基公,自動初始化一個。是不是很棒宋欺!
代碼是不是就到此結(jié)束了轰豆? 不是的。我們在開發(fā)代碼時需要考慮高效齿诞。Guava Cache在設(shè)計時也考慮到高效性酸休,不過如果不仔細閱讀使用文檔,也會給自己買坑掌挚。
Guava Cache清理什么時候發(fā)生雨席?使用CacheBuilder構(gòu)建的緩存不會"自動"執(zhí)行清理和回收工作,也不會在某個緩存項過期后馬上清理吠式,也沒有諸如此類的清理機制陡厘。相反,它會在寫操作時順帶做少量的維護工作特占,或者偶爾在讀操作時做——如果寫操作實在太少的話糙置。
如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作是目。如果你的 緩存只會偶爾有寫操作谤饭,而你又不想清理工作阻礙了讀操作,那么可以創(chuàng)建自己的維護線程懊纳,以固定的時間間隔調(diào)用Cache.cleanUp()揉抵。ScheduledExecutorService可以幫助你很好地實現(xiàn)這樣的定時調(diào)度。
對于高并發(fā)量的情況下嗤疯,我們還需要寫一個線程去定時cleanUp冤今。
Runnable metrciCacheCleanUpTask = new Runnable() {
@Override
public void run() {
getMetricCache().cleanUp();
} catch (Exception e) {
logger.error("定時cleanUp方法異常",e);
}
}
};
// 使用線程池每分鐘執(zhí)行一次
commTaskScheduler.scheduleWithFixedDelay(metrciCacheCleanUpTask, 60000);
線程池相關(guān)的實現(xiàn)可以參考我以前的blog,微分享-spring線程池實戰(zhàn)
Guava Cache CacheLoader還提供了數(shù)據(jù)加載機制茂缚,有興趣的話可以研究一下戏罢。