這篇文章給大家介紹Java中怎么設計本地緩存,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
1.數(shù)據(jù)結構
首要考慮的就是數(shù)據(jù)該如何存儲,用什么數(shù)據(jù)結構存儲,最簡單的就直接用Map來存儲數(shù)據(jù);或者復雜的如redis一樣提供了多種數(shù)據(jù)類型哈希,列表,集合,有序集合等,底層使用了雙端鏈表,壓縮列表,集合,跳躍表等數(shù)據(jù)結構;
2.對象上限
因為是本地緩存,內存有上限,所以一般都會指定緩存對象的數(shù)量比如1024,當達到某個上限后需要有某種策略去刪除多余的數(shù)據(jù);
3.清除策略
上面說到當達到對象上限之后需要有清除策略,常見的比如有LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用)等策略;
4.過期時間
除了使用清除策略,一般本地緩存也會有一個過期時間設置,比如redis可以給每個key設置一個過期時間,這樣當達到過期時間之后直接刪除,采用清除策略+過期時間雙重保證;
5.線程安全
像redis是直接使用單線程處理,所以就不存在線程安全問題;而我們現(xiàn)在提供的本地緩存往往是可以多個線程同時訪問的,所以線程安全是不容忽視的問題;并且線程安全問題是不應該拋給使用者去保證;
6.簡明的接口
提供一個傻瓜式的對外接口是很有必要的,對使用者來說使用此緩存不是一種負擔而是一種享受;提供常用的get,put,remove,clear,getSize方法即可;
7.是否持久化
這個其實不是必須的,是否需要將緩存數(shù)據(jù)持久化看需求;本地緩存如ehcache是支持持久化的,而guava是沒有持久化功能的;分布式緩存如redis是有持久化功能的,memcached是沒有持久化功能的;
8.阻塞機制
在看Mybatis源碼的時候,二級緩存提供了一個blocking標識,表示當在緩存中找不到元素時,它設置對緩存鍵的鎖定;這樣其他線程將等待此元素被填充,而不是命中數(shù)據(jù)庫;其實我們使用緩存的目的就是因為被緩存的數(shù)據(jù)生成比較費時,比如調用對外的接口,查詢數(shù)據(jù)庫,計算量很大的結果等等;這時候如果多個線程同時調用get方法獲取的結果都為null,每個線程都去執(zhí)行一遍費時的計算,其實也是對資源的浪費;比較好的辦法是只有一個線程去執(zhí)行,其他線程等待,計算一次就夠了;但是此功能基本上都交給使用者來處理,很少有本地緩存有這種功能;
如何實現(xiàn)
以上大致介紹了實現(xiàn)一個本地緩存我們都有哪些需要考慮的地方,當然可能還有其他沒有考慮到的點;下面繼續(xù)看看關于每個點都應該如何去實現(xiàn),重點介紹一下思路;
1.數(shù)據(jù)結構
本地緩存最常見的是直接使用Map來存儲,比如guava使用ConcurrentHashMap,ehcache也是用了ConcurrentHashMap,Mybatis二級緩存使用HashMap來存儲:
Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>()
Mybatis使用HashMap本身是非線程安全的,所以可以看到起內部使用了一個SynchronizedCache用來包裝,保證線程的安全性;
當然除了使用Map來存儲,可能還使用其他數(shù)據(jù)結構來存儲,比如redis使用了雙端鏈表,壓縮列表,整數(shù)集合,跳躍表和字典;當然這主要是因為redis對外提供的接口很豐富除了哈希還有列表,集合,有序集合等功能;
2.對象上限
本地緩存常見的一個屬性,一般緩存都會有一個默認值比如1024,在用戶沒有指定的情況下默認指定;當緩存的數(shù)據(jù)達到指定大值時,需要有相關策略從緩存中清除多余的數(shù)據(jù)這就涉及到下面要介紹的清除策略;
3.清除策略
配合對象上限之后使用,場景的清除策略如:LRU(最近最少使用)、FIFO(先進先出)、LFU(最近最不常用)、SOFT(軟引用)、WEAK(弱引用);
LRU:Least Recently Used的縮寫最近最少使用,移除最長時間不被使用的對象;常見的使用LinkedHashMap來實現(xiàn),也是很多本地緩存默認使用的策略;
FIFO:先進先出,按對象進入緩存的順序來移除它們;常見使用隊列Queue來實現(xiàn);
LFU:Least Frequently Used的縮寫大概也是最近最少使用的意思,和LRU有點像;區(qū)別點在LRU的淘汰規(guī)則是基于訪問時間,而LFU是基于訪問次數(shù)的;可以通過HashMap并且記錄訪問次數(shù)來實現(xiàn);
SOFT:軟引用基于垃圾回收器狀態(tài)和軟引用規(guī)則移除對象;常見使用SoftReference來實現(xiàn);
WEAK:弱引用更積極地基于垃圾收集器狀態(tài)和弱引用規(guī)則移除對象;常見使用WeakReference來實現(xiàn);
4.過期時間
設置過期時間,讓緩存數(shù)據(jù)在指定時間過后自動刪除;常見的過期數(shù)據(jù)刪除策略有兩種方式:被動刪除和主動刪除;
被動刪除:每次進行get/put操作的時候都會檢查一下當前key是否已經(jīng)過期,如果過期則刪除,類似如下代碼:
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
}
主動刪除:專門有一個job在后臺定期去檢查數(shù)據(jù)是否過期,如果過期則刪除,這其實可以有效的處理冷數(shù)據(jù);
5.線程安全
盡量用線程安全的類去存儲數(shù)據(jù),比如使用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.是否持久化
持久化的好處是重啟之后可以再次加載文件中的數(shù)據(jù),這樣就起到類似熱加載的功效;比如ehcache提供了是否持久化磁盤緩存的功能,將緩存數(shù)據(jù)存放在一個.data文件中;
diskPersistent="false" //是否持久化磁盤緩存
redis更是將持久化功能發(fā)揮到極致,慢慢的有點像數(shù)據(jù)庫了;提供了AOF和RDB兩種持久化方式;當然很多情況下可以配合使用兩種方式;
8.阻塞機制
除了在Mybatis中看到了BlockingCache來實現(xiàn)此功能,之前在看<<java并發(fā)編程實戰(zhàn)>>的時候其中有實現(xiàn)一個很完美的緩存,大致代碼如下:
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;
關于Java中怎么設計本地緩存就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
標題名稱:Java中怎么設計本地緩存-創(chuàng)新互聯(lián)
當前URL:http://chinadenli.net/article2/dgdeoc.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站營銷、自適應網(wǎng)站、域名注冊、手機網(wǎng)站建設、品牌網(wǎng)站制作、外貿網(wǎng)站建設
聲明:本網(wǎng)站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內容