學習java併發的時候,書上的例子是基於緩存展開的,因而就想能夠寫一個通用的本地緩存css
寫一個緩存,須要考慮緩存底層存儲結構、緩存過時、緩存失效、併發讀寫等問題,所以本身動手寫的本地緩存將圍繞這幾點進行設計java
緩存失效指的是緩存過時了,須要對過時的緩存數據進行刪除。刪除能夠分爲主動刪除和被動刪除兩種git
CPU
不友好,若是過時鍵值對比較多,刪除過時鍵值對會佔用至關一部分CPU
執行時間CPU
時間的影響【難點,執行時長和頻率比較難設置】CPU
友好CPU
和內存的平衡,所以本地緩存的緩存失效採用惰性刪除+按期刪除兩種緩存淘汰指的是緩存的數量達到必定值時按照某種規則刪除某個數據,不考慮該數據是否過時。常見的緩存淘汰算法有:github
FIFO
】LFU
】LRU
】 LRU
算法實現緩存淘汰選擇好了緩存失效和緩存淘汰的算法之後就能夠肯定緩存結構了,原先考略的是線程安全的K-V
結構的ConcurrentHashMap
再加+雙向鏈表的結構,但何甜甜最近沉迷記英語單詞,同時瞭解到LinkedHashMap
能夠實現LRU
,偷懶使用了LinkedHashMap
。LinkedHashMap
能夠基於插入順序存儲【默認】,也能夠根據訪問順序存儲【最近讀取的會放在最前面,最最不常讀取的會放在最後】,將插入順序存儲改成訪問順序存儲只需將accssOrder
設置爲true
便可,默認爲false
。同時LinkedHashMap
提供了一個用於判斷是否須要移除最不常讀取數據的方法【removeEldestEntry(Map.Entry<K, CacheNode<K, V>> eldest)
默認返回false
不移除】,須要移除重寫該方法就能夠了算法
public class CacheNode<K, V> {
/**
* 保存的鍵
*/
private K key;
/**
* 保存的值
*/
private V value;
/**
* 保存時間
*/
private long gmtCreate;
/**
* 過時時間,單位爲毫秒,默認永久有效
*/
private long expireTime = Long.MAX_VALUE;
}
複製代碼
/**
* 底層緩存結構
*/
private LinkedHashMap<K, CacheNode<K, V>> localCache;
/**
* 負載因子
*/
private final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 緩存過時清理策略
*/
private ExpireStrategy<K, V> lazyExpireStrategy = new LazyExpireStrategy<>();
private ExpireStrategy<K, V> regularExpireStrategy;
private int maxCacheSie;
/**
* 構造函數
*
* @param expireStrategy 緩存失效策略實現類,針對的是按期失效緩存,傳入null,按期失效緩存類爲默認配置值
* @param maxCacheSie 緩存最大容許存放的數量,緩存失效策略根據這個值觸發
*/
public LocalCache(int maxCacheSize, ExpireStrategy<K, V> expireStrategy) {
//緩存最大容量爲初始化的大小
this.maxCacheSize = maxCacheSize;
//緩存最大容量 => initialCapacity * DEFAULT_LOAD_FACTOR,避免擴容操做
int initialCapacity = (int) Math.ceil(maxCacheSie / DEFAULT_LOAD_FACTOR) + 1;
//accessOrder設置爲true,根據訪問順序而不是插入順序
this.localCache = new LinkedHashMap<K, CacheNode<K, V>>(initialCapacity, DEFAULT_LOAD_FACTOR, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, CacheNode<K, V>> eldest) {
return size() > maxCacheSie;
}
};
this.regularExpireStrategy = (expireStrategy == null ? new RegularExpireStrategy<>() : expireStrategy);
//啓動定時清除過時鍵任務
regularExpireStrategy.removeExpireKey(localCache, null);
}
複製代碼
說明:緩存
removeEldestEntry
方法,當緩存大小超過了設置的maxCacheSize
纔會移除不常使用的元素accessOrder
爲true
,根絕訪問順序存儲(int) Math.ceil(maxCacheSie / DEFAULT_LOAD_FACTOR) + 1
計算獲得,這樣即便達到設置的maxCacheSize
也不會觸發擴容操做regularExpireStrategy.removeExpireKey(localCache, null);
啓動按期刪除任務public class RegularExpireStrategy<K, V> implements ExpireStrategy<K, V> {
Logger logger = LoggerFactory.getLogger(getClass());
/**
* 按期任務每次執行刪除操做的次數
*/
private long executeCount = 100;
/**
* 按期任務執行時常 【1分鐘】
*/
private long executeDuration = 1000 * 60;
/**
* 按期任務執行的頻率
*/
private long executeRate = 60;
//get and set
public long getExecuteCount() {
return executeCount;
}
public void setExecuteCount(long executeCount) {
this.executeCount = executeCount;
}
public long getExecuteDuration() {
return executeDuration;
}
public void setExecuteDuration(long executeDuration) {
this.executeDuration = executeDuration;
}
public long getExecuteRate() {
return executeRate;
}
public void setExecuteRate(long executeRate) {
this.executeRate = executeRate;
}
/**
* 清空過時Key-Value
*
* @param localCache 本地緩存底層使用的存儲結構
* @param key 緩存的鍵
* @return 過時的值
*/
@Override
public V removeExpireKey(LinkedHashMap<K, CacheNode<K, V>> localCache, K key) {
logger.info("開啓按期清除過時key任務");
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
//定時週期任務,executeRate分鐘以後執行,默認1小時執行一次
executor.scheduleAtFixedRate(new MyTask(localCache), 0, executeRate, TimeUnit.MINUTES);
return null;
}
/**
* 自定義任務
*/
private class MyTask<K, V> implements Runnable {
private LinkedHashMap<K, CacheNode<K, V>> localCache;
public MyTask(LinkedHashMap<K, CacheNode<K, V>> localCache) {
this.localCache = localCache;
}
@Override
public void run() {
long start = System.currentTimeMillis();
List<K> keyList = localCache.keySet().stream().collect(Collectors.toList());
int size = keyList.size();
Random random = new Random();
for (int i = 0; i < executeCount; i++) {
K randomKey = keyList.get(random.nextInt(size));
if (localCache.get(randomKey).getExpireTime() - System.currentTimeMillis() < 0) {
logger.info("key:{}已過時,進行按期刪除key操做", randomKey);
localCache.remove(randomKey);
}
//超時執行退出
if (System.currentTimeMillis() - start > executeDuration) {
break;
}
}
}
}
}
複製代碼
說明:安全
ScheduledExecutorService
的scheduleAtFixedRate
實現定時週期任務public class LazyExpireStrategy<K, V> implements ExpireStrategy<K, V> {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* 清空過時Key-Value
*
* @param localCache 本地緩存底層使用的存儲結構
* @param key 緩存的鍵
* @return 過時的值
*/
@Override
public V removeExpireKey(LinkedHashMap<K, CacheNode<K, V>> localCache, K key) {
CacheNode<K, V> baseCacheValue = localCache.get(key);
//值不存在
if (baseCacheValue == null) {
logger.info("key:{}對應的value不存在", key);
return null;
} else {
//值存在而且未過時
if (baseCacheValue.getExpireTime() - System.currentTimeMillis() > 0) {
return baseCacheValue.getValue();
}
}
logger.info("key:{}已過時,進行懶刪除key操做", key);
localCache.remove(key);
return null;
}
}
複製代碼
說明:bash
null
值null
值public synchronized V removeKey(K key) {
CacheNode<K, V> cacheNode = localCache.remove(key);
return cacheNode != null ? cacheNode.getValue() : null;
}
複製代碼
public synchronized V getValue(K key) {
return lazyExpireStrategy.removeExpireKey(localCache, key);
}
複製代碼
查找的時候會走懶刪除策略public synchronized V putValue(K key, V value) {
CacheNode<K, V> cacheNode = new CacheNode<>();
cacheNode.setKey(key);
cacheNode.setValue(value);
localCache.put(key, cacheNode);
// 返回添加的值
return value;
}
複製代碼
存入的值失效:public synchronized V putValue(K key, V value, long expireTime) {
CacheNode<K, V> cacheNode = new CacheNode<>();
cacheNode.setKey(key);
cacheNode.setValue(value);
cacheNode.setGmtCreate(System.currentTimeMillis() + expireTime);
localCache.put(key, cacheNode);
// 返回添加的值
return value;
}
複製代碼
public synchronized void setExpireKey(K key, long expireTime) {
if (localCache.get(key) != null) {
localCache.get(key).setExpireTime(System.currentTimeMillis() + expireTime);
}
}
複製代碼
public synchronized int getLocalCacheSize() {
return localCache.size();
}
複製代碼
全部方法爲了保證線程安全都使用了synchronize
關鍵字【線程安全,何甜甜只會synchronize
,沒有想到其餘更好的加鎖方式、考慮了讀寫鎖可是行不通、、、】併發
LocalCache<Integer, Integer> localCache = new LocalCache<>(4, null);
複製代碼
第一個參數緩存的大小,容許存放緩存的數量 第二個參數按期刪除對象,若是爲null
,使用默認的按期刪除對象【執行週期、執行時間、執行次數都爲默認值】RegularExpireStrategy<Integer, Integer> expireStrategy = new RegularExpireStrategy<>();
expireStrategy.setExecuteRate(1); //每隔1分鐘執行一次
LocalCache<Integer, Integer> localCache = new LocalCache<>(4, expireStrategy);
複製代碼
傳入自定義的按期刪除對象for (int i = 0; i < 16; i++) {
localCache.putValue(i, i);
}
複製代碼
localCache.putValue(i, i,1000);
複製代碼
localCache.getValue(i)
複製代碼
localCache.setExpireKey(i, 1000)
複製代碼
localCache.getLocalCacheSize()
複製代碼
localCache.removeKey(i)
複製代碼
基於學習的目的寫了一個本地緩存,實際應用中仍是推薦使用Google
的Guava Cache
,若是你對個人代碼足夠自信,固然也歡迎使用提Bug
dom
ConcurrentHashMap
再加+雙向鏈表TimeUnit
參數時間選擇更多樣性最後附:項目完整代碼,歡迎fork,star 若有錯誤,歡迎指正交流【何甜甜真的太菜了!!!】