閒來無事,動手寫一個LRU本地緩存

學習java併發的時候,書上的例子是基於緩存展開的,因而就想能夠寫一個通用的本地緩存

寫在前面

寫一個緩存,須要考慮緩存底層存儲結構、緩存過時、緩存失效、併發讀寫等問題,所以本身動手寫的本地緩存將圍繞這幾點進行設計css

緩存失效

緩存失效指的是緩存過時了,須要對過時的緩存數據進行刪除。刪除能夠分爲主動刪除和被動刪除兩種java

主動刪除
  • 定時刪除

在設置鍵值對過時時間的同時,建立一個定時器,讓定時器在鍵過時時間來臨時,當即執行對鍵的刪除操做
優勢:對內存最友好,保證過時的鍵值對儘量地被刪除,釋放過時鍵值對所佔用的內存
缺點:對CPU不友好,若是過時鍵值對比較多,刪除過時鍵值對會佔用至關一部分CPU執行時間git

  • 按期刪除

每隔一段時間執行一次刪除過時鍵操做,並經過限制刪除操做執行的時長和頻率來減小刪除操做對CPU時間的影響【難點,執行時長和頻率比較難設置】github

被動刪除
  • 惰性刪除

只有在取出鍵的時候纔會對鍵進行過時檢查
優缺點和定時刪除相反
優勢:對CPU友好
缺點:對內存不友好
同時使用惰性刪除+按期刪除,能夠取得CPU和內存的平衡,所以本地緩存的緩存失效採用惰性刪除+按期刪除兩種算法

緩存淘汰

緩存淘汰指的是緩存的數量達到必定值時按照某種規則刪除某個數據,不考慮該數據是否過時。常見的緩存淘汰算法有:緩存

  • 先進先出算法【FIFO

最早存入緩存的數據將最早被淘汰
-最不常用算法【LFU
淘汰使用次數最少的數據,通常實現是對每一個數據進行計數,每使用一次就進行計算一次,淘汰計數次數最少的安全

  • 最近最少使用算法【LRU

最近不使用的數據最早被淘汰,通常實現是經過鏈表,將最新訪問、新插入的元素移到鏈表頭部,淘汰鏈表最後一個元素
本地緩存將選擇LRU算法實現緩存淘汰併發

緩存結構定義

選擇好了緩存失效和緩存淘汰的算法之後就能夠肯定緩存結構了,原先考略的是線程安全的K-V結構的ConcurrentHashMap再加+雙向鏈表的結構,但何甜甜最近沉迷記英語單詞,同時瞭解到LinkedHashMap能夠實現LRU,偷懶使用了LinkedHashMapLinkedHashMap能夠基於插入順序存儲【默認】,也能夠根據訪問順序存儲【最近讀取的會放在最前面,最最不常讀取的會放在最後】,將插入順序存儲改成訪問順序存儲只需將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纔會移除不常使用的元素
  • 構造函數中設置accessOrdertrue,根絕訪問順序存儲
  • 緩存容量大小由(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;
                }
            }
        }
    }
}

說明:

  • 使用ScheduledExecutorServicescheduleAtFixedRate實現定時週期任務
  • 默認1小時執行一次,每次執行的時間爲1分鐘,每次隨機嘗試刪除100個元素【若是時間容許、鍵過時】

懶加載刪除策略實現:LazyExpireStrategy.java

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)

基於學習的目的寫了一個本地緩存,實際應用中仍是推薦使用GoogleGuava Cache,若是你對個人代碼足夠自信,固然也歡迎使用提Bug

優化點

  • 使用ConcurrentHashMap再加+雙向鏈表
  • 緩存失效、定時任務時間選擇多樣性,目前使用緩存失效單位默認爲毫秒,定時任務默認單位爲分鐘,經過在方法中加入TimeUnit參數時間選擇更多樣性
  • 併發支持較差,實現的是同步【何甜甜太菜了!!!】

TODO

  • 上傳私服,提供依賴的方式在其餘項目中使用,順便學習一下如何上傳私服



最後附:項目完整代碼,歡迎forkstar 若有錯誤,歡迎指正交流【何甜甜真的太菜了!!!】

相關文章
相關標籤/搜索