學習java併發的時候,書上的例子是基於緩存展開的,因而就想能夠寫一個通用的本地緩存
寫一個緩存,須要考慮緩存底層存儲結構、緩存過時、緩存失效、併發讀寫等問題,所以本身動手寫的本地緩存將圍繞這幾點進行設計css
緩存失效指的是緩存過時了,須要對過時的緩存數據進行刪除。刪除能夠分爲主動刪除和被動刪除兩種java
在設置鍵值對過時時間的同時,建立一個定時器,讓定時器在鍵過時時間來臨時,當即執行對鍵的刪除操做
優勢:對內存最友好,保證過時的鍵值對儘量地被刪除,釋放過時鍵值對所佔用的內存
缺點:對CPU
不友好,若是過時鍵值對比較多,刪除過時鍵值對會佔用至關一部分CPU
執行時間git
每隔一段時間執行一次刪除過時鍵操做,並經過限制刪除操做執行的時長和頻率來減小刪除操做對CPU
時間的影響【難點,執行時長和頻率比較難設置】github
只有在取出鍵的時候纔會對鍵進行過時檢查
優缺點和定時刪除相反
優勢:對CPU
友好
缺點:對內存不友好
同時使用惰性刪除+按期刪除,能夠取得CPU
和內存的平衡,所以本地緩存的緩存失效採用惰性刪除+按期刪除兩種算法
緩存淘汰指的是緩存的數量達到必定值時按照某種規則刪除某個數據,不考慮該數據是否過時。常見的緩存淘汰算法有:緩存
FIFO
】最早存入緩存的數據將最早被淘汰
-最不常用算法【LFU
】
淘汰使用次數最少的數據,通常實現是對每一個數據進行計數,每使用一次就進行計算一次,淘汰計數次數最少的安全
LRU
】最近不使用的數據最早被淘汰,通常實現是經過鏈表,將最新訪問、新插入的元素移到鏈表頭部,淘汰鏈表最後一個元素
本地緩存將選擇LRU
算法實現緩存淘汰併發
選擇好了緩存失效和緩存淘汰的算法之後就能夠肯定緩存結構了,原先考略的是線程安全的K-V
結構的ConcurrentHashMap
再加+雙向鏈表的結構,但何甜甜最近沉迷記英語單詞,同時瞭解到LinkedHashMap
能夠實現LRU
,偷懶使用了LinkedHashMap
。LinkedHashMap
能夠基於插入順序存儲【默認】,也能夠根據訪問順序存儲【最近讀取的會放在最前面,最最不常讀取的會放在最後】,將插入順序存儲改成訪問順序存儲只需將accssOrder
設置爲true
便可,默認爲false
。同時LinkedHashMap
提供了一個用於判斷是否須要移除最不常讀取數據的方法【removeEldestEntry(Map.Entry<K, CacheNode<K, V>> eldest)
默認返回false
不移除】,須要移除重寫該方法就能夠了dom
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 maxCacheSie, ExpireStrategy<K, V> expireStrategy) { //緩存最大容量爲初始化的大小 this.maxCacheSie = maxCacheSie; //緩存最大容量 => 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); }
說明:ide
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; } }
說明:
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對象
姿式一
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
ConcurrentHashMap
再加+雙向鏈表TimeUnit
參數時間選擇更多樣性最後附:項目完整代碼,歡迎fork,star 若有錯誤,歡迎指正交流【何甜甜真的太菜了!!!】