Java技術分享:如何設計一個本地快取?
前言
最近在看Mybatis的原始碼,剛好看到快取這一塊,Mybatis提供了一級快取和二級快取;一級快取相對來說比較簡單,功能比較齊全的是二級快取,基本上滿足了一個快取該有的功能;當然如果拿來和專門的快取框架如ehcache來對比可能稍有差距;本文小千帶大家來整理一下實現一個本地快取都應該需要考慮哪些東西。
考慮點
考慮點主要在資料用何種方式儲存,能儲存多少資料,多餘的資料如何處理等幾個點,下面我們來詳細的介紹每個考慮點,以及該如何去實現;
1.資料結構
首要考慮的就是資料該如何儲存,用什麼資料結構儲存,最簡單的就直接用Map來儲存資料;或者複雜的如redis一樣提供了多種資料型別雜湊,列表,集合,有序集合等,底層使用了雙端連結串列,壓縮列表,集合,跳躍表等資料結構;
2.物件上限
因為是本地快取,記憶體有上限,所以一般都會指定快取物件的數量比如1024,當達到某個上限後需要有某種策略去刪除多餘的資料;
3.清除策略
上面說到當達到物件上限之後需要有清除策略,常見的比如有LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用)等策略;
4.過期時間
除了使用清除策略,一般本地快取也會有一個過期時間設定,比如redis可以給每個key設定一個過期時間,這樣當達到過期時間之後直接刪除,採用清除策略+過期時間雙重保證;
5.執行緒安全
像redis是直接使用單執行緒處理,所以就不存線上程安全問題;而我們現在提供的本地快取往往是可以多個執行緒同時訪問的,所以執行緒安全是不容忽視的問題;並且執行緒安全問題是不應該拋給使用者去保證;
6.簡明的介面
提供一個傻瓜式的對外介面是很有必要的,對使用者來說使用此快取不是一種負擔而是一種享受;提供常用的get,put,remove,clear,getSize方法即可;
7.是否持久化
這個其實不是必須的,是否需要將快取資料持久化看需求;本地快取如ehcache是支援持久化的,而guava是沒有持久化功能的;分散式快取如redis是有持久化功能的,memcached是沒有持久化功能的;
8.阻塞機制
在看Mybatis原始碼的時候,二級快取提供了一個blocking標識,表示當在快取中找不到元素時,它設定對快取鍵的鎖定;這樣其他執行緒將等待此元素被填充,而不是命中資料庫;其實我們使用快取的目的就是因為被快取的資料生成比較費時,比如呼叫對外的介面,查詢資料庫,計算量很大的結果等等;這時候如果多個執行緒同時呼叫get方法獲取的結果都為null,每個執行緒都去執行一遍費時的計算,其實也是對資源的浪費;比較好的辦法是隻有一個執行緒去執行,其他執行緒等待,計算一次就夠了;但是此功能基本上都交給使用者來處理,很少有本地快取有這種功能;
如何實現
以上大致介紹了實現一個本地快取我們都有哪些需要考慮的地方,當然可能還有其他沒有考慮到的點;下面繼續看看關於每個點都應該如何去實現,重點介紹一下思路;
1.資料結構
本地快取最常見的是直接使用Map來儲存,比如guava使用ConcurrentHashMap,ehcache也是用了ConcurrentHashMap,Mybatis二級快取使用HashMap來儲存:
Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>()
Mybatis使用HashMap本身是非執行緒安全的,所以可以看到起內部使用了一個SynchronizedCache用來包裝,保證執行緒的安全性;
當然除了使用Map來儲存,可能還使用其他資料結構來儲存,比如redis使用了雙端連結串列,壓縮列表,整數集合,跳躍表和字典;當然這主要是因為redis對外提供的介面很豐富除了雜湊還有列表,集合,有序集合等功能;
2.物件上限
本地快取常見的一個屬性,一般快取都會有一個預設值比如1024,在使用者沒有指定的情況下預設指定;當快取的資料達到指定最大值時,需要有相關策略從快取中清除多餘的資料這就涉及到下面要介紹的清除策略;
3.清除策略
配合物件上限之後使用,場景的清除策略如:LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用);
LRU:Least Recently Used的縮寫最近最少使用,移除最長時間不被使用的物件;常見的使用LinkedHashMap來實現,也是很多本地快取預設使用的策略;
FIFO:先進先出,按物件進入快取的順序來移除它們;常見使用佇列Queue來實現;
LFU:Least Frequently Used的縮寫大概也是最近最少使用的意思,和LRU有點像;區別點在LRU的淘汰規則是基於訪問時間,而LFU是基於訪問次數的;可以透過HashMap並且記錄訪問次數來實現;
SOFT:軟引用基於垃圾回收器狀態和軟引用規則移除物件;常見使用SoftReference來實現;
WEAK:弱引用更積極地基於垃圾收集器狀態和弱引用規則移除物件;常見使用WeakReference來實現;
4.過期時間
設定過期時間,讓快取資料在指定時間過後自動刪除;常見的過期資料刪除策略有兩種方式:被動刪除和主動刪除;
被動刪除:每次進行get/put操作的時候都會檢查一下當前key是否已經過期,如果過期則刪除,類似如下程式碼:
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
}
主動刪除:專門有一個job在後臺定期去檢查資料是否過期,如果過期則刪除,這其實可以有效的處理冷資料;
5.執行緒安全
儘量用執行緒安全的類去儲存資料,比如使用ConcurrentHashMap代替HashMap;或者提供相應的同步處理類,比如Mybatis提供了SynchronizedCache:
public synchronized void putObject(Object key, Object object) {
...省略...
}
@Override
public synchronized Object getObject(Object key) {
...省略...
}
6.簡明的介面
提供常用的get,put,remove,clear,getSize方法即可,比如Mybatis的Cache介面:
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
ReadWriteLock getReadWriteLock();
}
再來看看guava提供的Cache介面,相對來說也是比較簡潔的:
public interface Cache<K, V> {
V getIfPresent(@CompatibleWith("K") Object key);
V get(K key, Callable<? extends V> loader) throws ExecutionException;
ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
void put(K key, V value);
void putAll(Map<? extends K, ? extends V> m);
void invalidate(@CompatibleWith("K") Object key);
void invalidateAll(Iterable<?> keys);
void invalidateAll();
long size();
CacheStats stats();
ConcurrentMap<K, V> asMap();
void cleanUp();
}
7.是否持久化
持久化的好處是重啟之後可以再次載入檔案中的資料,這樣就起到類似熱載入的功效;比如ehcache提供了是否持久化磁碟快取的功能,將快取資料存放在一個.data檔案中;
diskPersistent="false" //是否持久化磁碟快取
redis更是將持久化功能發揮到極致,慢慢的有點像資料庫了;提供了AOF和RDB兩種持久化方式;當然很多情況下可以配合使用兩種方式;
8.阻塞機制
除了在Mybatis中看到了BlockingCache來實現此功能,之前在看<<java併發程式設計實戰>>的時候其中有實現一個很完美的快取,大致程式碼如下:
public class Memoizerl<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizerl(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException, ExecutionException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
}
}
}
}
}
compute是一個計算很費時的方法,所以這裡把計算的結果快取起來,但是有個問題就是如果兩個執行緒同時進入此方法中怎麼保證只計算一次,這裡最核心的地方在於使用了ConcurrentHashMap的putIfAbsent方法,同時只會寫入一個FutureTask;
總結:要設計一個本地快取都需要考慮哪些點:資料結構,物件上限,清除策略,過期時間,執行緒安全,阻塞機制,實用的介面,是否持久化;當然肯定有其他考慮點,歡迎補充。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31548651/viewspace-2769039/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 好程式設計師Java培訓分享本地快取如何設計程式設計師Java快取
- 【轉】設計一個iOS應用的本地快取機制iOS快取
- 小工匠聊架構 - 分散式快取技術_快取設計架構分散式快取
- 固定容量的本地快取設計快取
- [.net 物件導向程式設計進階] (14) 快取(Cache) (一) 認識快取技術物件程式設計快取
- 技術分享| 快對講如何降噪
- 快取技術快取
- 技術分享| 快對講排程系統設計概要
- Vue 全站快取二:如何設計全站快取Vue快取
- 用Java寫一個分散式快取——快取管理Java分散式快取
- 技術分享| 快對講融合影片監控功能設計
- 使用ThreadLocal來實現一個本地快取thread快取
- 技術變化那麼快,Java程式設計師如何做到不被淘汰?Java程式設計師
- Java技術分享之函數語言程式設計!Java函數程式設計
- Java技術分享之函數語言程式設計Java函數程式設計
- 好程式設計師Java培訓分享Java之反射技術程式設計師Java反射
- 從 YYCache 原始碼 Get 到如何設計一個優秀的快取原始碼快取
- Python快取技術Python快取
- 位元組快取技術快取
- 快取技術淺談快取
- ASP快取技術 (轉)快取
- 如何設計快取系統:快取穿透,快取擊穿,快取雪崩解決方案分析快取穿透
- Java高效能本地快取框架CaffeineJava快取框架
- Java技術分享:小白如何入門Mybatis?JavaMyBatis
- 好程式設計師Java培訓分享Redis快取使用場景概述程式設計師JavaRedis快取
- 分享一個 Trait 來易用 Laravel 的快取AILaravel快取
- JAVA中使用最廣泛的本地快取?Ehcache的自信從何而來3 —— 本地快取變身分散式叢集快取,打破本地快取天花板Java快取分散式
- 前端常用的快取技術前端快取
- 快取技術方案改造思考快取
- 用Java寫一個分散式快取——快取淘汰演算法Java分散式快取演算法
- 聊聊本地快取和分散式快取快取分散式
- 好程式設計師Java教程分享Java技術知識點總結程式設計師Java
- 好程式設計師Java教程分享Java面試常見技術難題程式設計師Java面試
- 快取之美——如何選擇合適的本地快取?快取
- 一個JAVA程式設計師成長之路分享Java程式設計師
- Redis 快取雪崩,快取擊穿和快取穿透技術方案總結Redis快取穿透
- 【ionic】storage本地快取快取
- 淺談快取寫法(三):記憶體快取該如何設計快取記憶體