java 快取架構剖析--本地快取(LoadingCache)
快取的使用可以大大提高程式的執行效率,但是如果快取無法及時更新會導致髒讀的情況。
痛點剖析:
記得早期我呆過的一家公司有個核心服務是在啟動的時候一下把常用的交易配置資訊是從DB查出來放在Map裡面來做快取,先不考慮其他的,如果我想更新一下交易配置資訊是不是需要每次都重啟伺服器呢,又或者說我開幾個後門介面用來更新Map資訊,這樣不還得考慮執行緒安全的問題麼。
好吧,我先上個在中小型專案中,乃至大型專案中也常用的快取架構,如下:
記憶體架構圖
我大概解釋一下流程吧:
1、系統A中使用LoadingCache來維護本地快取資訊
2、當快取重新整理時(同步、非同步)呼叫B系統來更新快取資訊
3、系統B接收A獲取配置資料的請求,如果redis快取中有資料就直接從redis中拿
4、當快取中不存在請求則穿透到DB裡面查詢再將結果塞到redis,並返回結果
5、其實還有一步沒畫出來應該是有個定時job輪詢DB配置資訊變化時重新整理redis資訊(或者訊息機制來實現快取更新)
言歸正傳,下面來詳解一下LoadingCache的使用:
public static LoadingCache<String,String> cahceBuilder = CacheBuilder.newBuilder().maximumSize(1).
// expireAfterWrite(1, TimeUnit.SECONDS)
.refreshAfterWrite(2, TimeUnit.MILLISECONDS)
.removalListener(new RemovalListener() {
@Override
public void onRemoval(RemovalNotificationrn) {
System.out.println(rn.getKey() + "被移除");}})
.build(new CacheLoader() {
@Override
public String load(String key) throws Exception {
String strProValue = "hello " + key + "!";
System.out.println("%%%%%" + strProValue);
return strProValue;
}});
public static void main(String[] args) throws ExecutionException, InterruptedException {
cahceBuilder.get("jerry");
cahceBuilder.get("peida");
Thread.sleep(1000);
cahceBuilder.get("jerry1");
}
輸出結果為:
%%%%%hello jerry! -- 在第一次get的時候沒有值會執行load方法,去取值然後塞到本地快取
%%%%%hello peida! -- 在第一次get的時候沒有值會執行load方法,去取值然後塞到本地快取
jerry被移除 -- maximumSize(1) 最大值為1,當預儲存第二個值的時候第一個值會被移除
%%%%%hello jerry1! -- refreshAfterWrite設定2ms自動定時重新整理,當有訪問時會重新執行load方法更新快取
peida被移除 -- maximumSize(1) 最大值為1,當預儲存第二個值的時候第一個值會被移除
方法剖析:
get(K):這個方法要麼返回已經快取的值,要麼使用CacheLoader向快取原子地loading新值(就是上面說的當快取沒有值的時候執行Load方法)
put(key, value):這個方法可以直接顯示地向快取中插入值,這會直接覆蓋掉已有鍵之前對映的值。
快取回收:
CacheBuilder.maximumSize(long):這個方法規定快取項的數目不超過固定值(其實你可以理解為一個Map的最大容量),嘗試回收最近沒有使用或總體上很少使用的快取項
定時回收(Timed Eviction):
expireAfterAccess(long, TimeUnit):快取項在給定時間內沒有被讀/寫訪問,則回收。請注意這種快取的回收順序和基於大小回收一樣。
expireAfterWrite(long, TimeUnit):快取項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。如果認為快取資料總是在固定時候後變得陳舊不可用,這種回收方式是可取的。
顯式清除:
任何時候,你都可以顯式地清除快取項,而不是等到它被回收:
移除監聽器
就如我上面的例子一樣,當記憶體回收或者定時回收都會執行removalListener
不過親測當有資料refresh重新整理額度時候也會觸發這個監聽功能
警告:預設情況下,監聽器方法是在移除快取時同步呼叫的。因為快取的維護和請求響應通常是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的快取請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監聽器裝飾為非同步操作。
重新整理,這應該會是我重點講的:
LoadingCache.refresh(K):重新整理和回收不太一樣。重新整理表示為鍵載入新值,這個過程可以是非同步的。在重新整理操作進行時,快取仍然可以向其他執行緒返回舊值,而不像回收操作,讀快取的執行緒必須等待新值載入完成。如果重新整理過程丟擲異常,快取將保留舊值,而異常會在記錄到日誌後被丟棄[swallowed]。過載CacheLoader.reload(K, V)可以擴充套件重新整理時的行為,這個方法允許開發者在計算新值時使用舊的值。
CacheBuilder.refreshAfterWrite(long, TimeUnit):可以為快取增加自動定時重新整理功能。和expireAfterWrite相反,refreshAfterWrite通過定時重新整理可以讓快取項保持可用,但請注意:快取項只有在被檢索時才會真正重新整理,即只有重新整理間隔時間到了你再去get(key)才會重新去執行Loading否則就算重新整理間隔時間到了也不會執行loading操作。因此,如果你在快取上同時宣告expireAfterWrite和refreshAfterWrite,快取並不會因為重新整理盲目地定時重置,如果快取項沒有被檢索,那重新整理就不會真的發生,快取項在過期時間後也變得可以回收。還有一點比較重要的是refreshAfterWrite和expireAfterWrite兩個方法設定以後,重新get會引起loading操作都是同步序列的。這其實可能會有一個隱患,當某一個時間點剛好有大量檢索過來而且都有重新整理或者回收的話,是會產生大量的請求同步呼叫loading方法,這些請求佔用執行緒資源的時間明顯變長。如正常請求也就20ms,當重新整理以後加上同步請求loading這個功能介面可能響應時間遠遠大於20ms。
還是上程式碼:
public class LoadingCacheTest {
private static ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private static class EchoServer implements Runnable {
@Override
public void run() {
try {
Set<String> keys = cahceBuilder.asMap().keySet();
for(String key : keys){
cahceBuilder.refresh(key);
}
}catch (Exception e) {
}
}}
public static LoadingCachecahceBuilder = CacheBuilder.newBuilder().maximumSize(10).
removalListener(new RemovalListener() {
@Override
public void onRemoval(RemovalNotificationrn) {
System.out.println(rn.getKey() + "被移除");
}).build(new CacheLoader() {
@Override
public String load(String key) throws Exception {
String strProValue = "hello " + key + "!";
System.out.println("%%%%%" + strProValue);
return strProValue;
}});
public static void main(String[] args) throws Execution {
System.out.println(cahceBuilder.get("jerry"));
System.out.println(cahceBuilder.get("peida"));
cahceBuilder.get("jerry1");
executor.scheduleAtFixedRate(new EchoServer(),0,1000,TimeUnit.MILLISECONDS);
}}
其他特性
統計
此外,還有其他很多統計資訊。這些統計資訊對於調整快取設定是至關重要的,在效能要求高的應用中我們建議密切關注這些資料。
asMap檢視
asMap檢視提供了快取的ConcurrentMap形式,但asMap檢視與快取的互動需要注意:
cache.asMap()包含當前所有載入到快取的項。因此相應地,cache.asMap().keySet()包含當前所有已載入鍵;
asMap().get(key)實質上等同於cache.getIfPresent(key),而且不會引起快取項的載入。這和Map的語義約定一致。
所有讀寫操作都會重置相關快取項的訪問時間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合檢視上的操作。比如,遍歷Cache.asMap().entrySet()不會重置快取項的讀取時間。
最後,歡迎一起討論,謝謝!
作者:hello_coke
連結:https://www.jianshu.com/p/f4b99b70bd76
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。