面試官:如何設(shè)計(jì)和實(shí)現(xiàn)一個(gè)帶過(guò)期時(shí)間的本地緩存?
在日常開發(fā)中有很多這樣的場(chǎng)景:有一些業(yè)務(wù)系統(tǒng)的配置信息,數(shù)據(jù)量不大,修改頻率不高,但是訪問(wèn)很頻繁。如果每次程序都從數(shù)據(jù)庫(kù)或集中式緩存中獲取,受限于硬盤 I/O性能、遠(yuǎn)程網(wǎng)絡(luò)訪問(wèn)限制等,程序的執(zhí)行效率不高。在這樣的業(yè)務(wù)場(chǎng)景中,我們可以通過(guò)本地緩存來(lái)提升數(shù)據(jù)訪問(wèn)的效率。
今天我們來(lái)基于ConcurrentHashMap與ScheduledThreadPoolExecutor來(lái)實(shí)現(xiàn)一個(gè)線程安全的本地緩存:LocalCache。在LocalCache中支持永久緩存與臨時(shí)緩存,永久緩存的數(shù)據(jù)一直有效,臨時(shí)緩存的數(shù)據(jù)在指定時(shí)間到期之后會(huì)自動(dòng)從緩存中移出。
LocalCache提供了數(shù)據(jù)安全的增、刪、改、查功能,具體方法如下所示:
方法名稱 | 方法說(shuō)明 |
put(String key , V value) | 向緩存中插入數(shù)據(jù),數(shù)據(jù)永久有效 |
put(String key , V value , int seconds) | 向緩存中插入數(shù)據(jù),數(shù)據(jù)根據(jù)設(shè)定的時(shí)間生效,時(shí)間到期會(huì)從緩存中移出 |
containKey(String key) | 判斷緩存中是否包含對(duì)應(yīng)的key |
get(String key) | 根據(jù)key從緩存中獲取數(shù)據(jù) |
remove(String key) | 移出緩存中對(duì)應(yīng)key的數(shù)據(jù) |
shutdownNow() | 關(guān)閉緩存池 |
1. 設(shè)計(jì)原理
LocalCache主要由3個(gè)部分組成:數(shù)據(jù)緩存、數(shù)據(jù)超時(shí)時(shí)間、數(shù)據(jù)清理任務(wù)。數(shù)據(jù)緩存和數(shù)據(jù)超時(shí)時(shí)間都采用ConcurrentHashMap來(lái)存儲(chǔ)數(shù)據(jù),數(shù)據(jù)超時(shí)時(shí)間中Key為數(shù)據(jù)存儲(chǔ)的鍵,value是數(shù)據(jù)的時(shí)間戳。數(shù)據(jù)清理任務(wù)采用ScheduledThreadPoolExecutor實(shí)現(xiàn)任務(wù)調(diào)度,默認(rèn)的任務(wù)線程數(shù)為1,這樣可以避免多線程帶來(lái)的并發(fā)修改問(wèn)題,同時(shí)線程都是內(nèi)存操作,這樣單線程同樣具備高性能。
本地緩存的設(shè)計(jì)如下圖所示:
圖片
每次項(xiàng)緩存中插入數(shù)據(jù)時(shí),LocalCache首先會(huì)將數(shù)據(jù)插入到ConcurrentHashMap中。然后判斷有沒有設(shè)置超時(shí)時(shí)間,如果有超時(shí)時(shí)間,LocalCache會(huì)將失效時(shí)間插入到ConcurrentHashMap中,并創(chuàng)建數(shù)據(jù)清理任務(wù),之后任務(wù)提交到ScheduledThreadPoolExecutor線程池中。
每次從緩存中查詢數(shù)據(jù),LocalCache會(huì)直接從ConcurrentHashMap中讀取數(shù)據(jù)。
定時(shí)任務(wù)線程池會(huì)按照超時(shí)時(shí)間來(lái)觸發(fā)數(shù)據(jù)清理任務(wù),數(shù)據(jù)清理任務(wù)會(huì)從數(shù)據(jù)時(shí)長(zhǎng)的緩存池中獲取Key對(duì)應(yīng)的時(shí)間,判斷當(dāng)前Key對(duì)應(yīng)的數(shù)據(jù)是否已經(jīng)到期了。如果數(shù)據(jù)已經(jīng)到期了,LocalCache會(huì)調(diào)用remove方法將數(shù)據(jù)從緩存池中移除。
2. 實(shí)現(xiàn)方案
LocalCache作為本地緩存的接口,定義了數(shù)據(jù)插入、數(shù)據(jù)刪除、數(shù)據(jù)查詢的相關(guān)接口方法。DefaultLocalCache 定義了兩個(gè)ConcurrentHashMap變量:dataMap和timeOutMap。dataMap用來(lái)緩存數(shù)據(jù)信息,timeOutMap用來(lái)存儲(chǔ)數(shù)據(jù)失效的時(shí)間戳,同時(shí)還定義了數(shù)據(jù)清理任務(wù)ClearTask,ClearTask負(fù)責(zé)將過(guò)期的數(shù)據(jù)從dataMap中移除。UML圖如下所示:
圖片
3. 代碼展示
3.1 接口定義
public interface LocalCache<V> {
/**
* 插入數(shù)據(jù),數(shù)據(jù)永久有效
*/
boolean put(String key, V value);
/**
* 插入數(shù)據(jù),在指定時(shí)間內(nèi)生效
*/
boolean put(String key, V value, int seconds);
/**
* 是否包含指定的key
*/
boolean containKey(String key);
/**
* 獲取指定Key的值
*/
V get(String key);
/**
* 從緩存中移除key對(duì)應(yīng)的數(shù)據(jù)
*/
void remove(String key);
void shutdownNow();
}
在接口LocalCache中定義了兩個(gè)數(shù)據(jù)插入的put接口:一個(gè)沒有到期時(shí)間,另一個(gè)有到期時(shí)間。沒有到期時(shí)間表示數(shù)據(jù)永久有效,有到期時(shí)間的數(shù)據(jù)會(huì)在到期后從緩存中移除。
接口實(shí)現(xiàn)
在接口實(shí)現(xiàn)DefaultLocalCache內(nèi)部定義了三個(gè)常量:緩存的默認(rèn)大小DEFAULT_CAPACITY、最大容量MAX_CAPACITY、定時(shí)線程池的大小DEFAULT_THREAD_SIZE。核心代碼如下:
public class DefaultLocalCache<V> implements LocalCache<V> {
// 默認(rèn)容量
private static final int DEFAULT_CAPACITY = 1024;
private static final int MAX_CAPACITY = 100000;
private static final int DEFAULT_THREAD_SIZE = 1;
private final int maxSize;
//數(shù)據(jù)map
private volatile ConcurrentHashMap<String,V> dataMap;
//過(guò)期時(shí)間
private final ConcurrentHashMap<String,Long> timeOutMap;
//定時(shí)任務(wù)
private final ScheduledExecutorService executorService;
public DefaultLocalCache() {
maxSize = MAX_CAPACITY;
dataMap = new ConcurrentHashMap<>(DEFAULT_CAPACITY);
timeOutMap = new ConcurrentHashMap<>(DEFAULT_CAPACITY);
executorService = new ScheduledThreadPoolExecutor(DEFAULT_THREAD_SIZE) ;
}
public DefaultLocalCache(int size) {
maxSize = size;
dataMap = new ConcurrentHashMap<>(DEFAULT_CAPACITY);
timeOutMap = new ConcurrentHashMap<>(DEFAULT_CAPACITY);
executorService = new ScheduledThreadPoolExecutor(DEFAULT_THREAD_SIZE) ;
}
@Override
public boolean put(String key, V value) {
//檢查容量
if(checkCapacity()){
dataMap.put(key,value);
return true;
}
return false;
}
@Override
public boolean put(String key, V value, int seconds) {
if(checkCapacity()){
dataMap.put(key,value);
if(seconds >= 0){
timeOutMap.put(key,getTimeOut(seconds));
ClearTask task = new ClearTask(key);
executorService.schedule(task, seconds, TimeUnit.SECONDS);
}
}
return false;
}
......
class ClearTask implements Runnable{
private String key;
public ClearTask(String key){
this.key = key;
}
@Override
public void run() {
//判斷緩存中是否有key
if(timeOutMap.contains(key)){
//獲取失效時(shí)間
Long expire = timeOutMap.get(key);
//如果失效時(shí)間大于0,并且比當(dāng)前時(shí)間小,則刪除緩存
if(expire > 0){
long now = System.currentTimeMillis();
if(now >= expire){
remove(key);
}
}
}
}
}
}
在LocalCache的默認(rèn)實(shí)現(xiàn)DefaultLocalCache中,基于ConcurrentHashMap與ScheduledThreadPoolExecutor結(jié)合使用,使得LocalCache支持永久緩存與臨時(shí)緩存兩種能力。