Java實現緩存(LRU,FIFO)

編程語言 Java Memcached 軟件 科技優家 2017-04-06

現在軟件或者網頁的併發量越來越大了,大量請求直接操作數據庫會對數據庫造成很大的壓力,處理大量連接和請求就會需要很長時間,但是實際中百分之80的數據是很少更改的,這樣就可以引入緩存來進行讀取,減少數據庫的壓力。

常用的緩存有Redis和memcached,但是有時候一些小場景就可以直接使用Java實現緩存,就可以滿足這部分服務的需求。

緩存主要有LRU和FIFO,LRU是Least Recently Used的縮寫,即最近最久未使用,FIFO就是先進先出,下面就使用Java來實現這兩種緩存。

LRU

LRU緩存的思想

  • 固定緩存大小,需要給緩存分配一個固定的大小。
  • 每次讀取緩存都會改變緩存的使用時間,將緩存的存在時間重新刷新。
  • 需要在緩存滿了後,將最近最久未使用的緩存刪除,再添加最新的緩存。

按照如上思想,可以使用LinkedHashMap來實現LRU緩存。

這是LinkedHashMap的一個構造函數,傳入的第三個參數accessOrder為true的時候,就按訪問順序對LinkedHashMap排序,為false的時候就按插入順序,默認是為false的。 當把accessOrder設置為true後,就可以將最近訪問的元素置於最前面,這樣就可以滿足上述的第二點。

/**
 * Constructs an empty <tt>LinkedHashMap</tt> instance with the
 * specified initial capacity, load factor and ordering mode.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @param  accessOrder     the ordering mode - <tt>true</tt> for
 *         access-order, <tt>false</tt> for insertion-order
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public LinkedHashMap(int initialCapacity,
 float loadFactor,
 boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

這是LinkedHashMap中另外一個方法,當返回true的時候,就會remove其中最久的元素,可以通過重寫這個方法來控制緩存元素的刪除,當緩存滿了後,就可以通過返回true刪除最久未被使用的元素,達到LRU的要求。這樣就可以滿足上述第三點要求。

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

由於LinkedHashMap是為自動擴容的,當table數組中元素大於Capacity * loadFactor的時候,就會自動進行兩倍擴容。但是為了使緩存大小固定,就需要在初始化的時候傳入容量大小和負載因子。

為了使得到達設置緩存大小不會進行自動擴容,需要將初始化的大小進行計算再傳入,可以將初始化大小設置為(緩存大小 / loadFactor) + 1,這樣就可以在元素數目達到緩存大小時,也不會進行擴容了。這樣就解決了上述第一點問題。

通過上面分析,實現下面的代碼

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class LRU1<K, V> {
    private final int MAX_CACHE_SIZE;
    private final float DEFAULT_LOAD_FACTORY = 0.75f;

    LinkedHashMap<K, V> map;

    public LRU1(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;
        int capacity = (int)Math.ceil(MAX_CACHE_SIZE / DEFAULT_LOAD_FACTORY) + 1;
        /*
         * 第三個參數設置為true,代表linkedlist按訪問順序排序,可作為LRU緩存
         * 第三個參數設置為false,代表按插入順序排序,可作為FIFO緩存
         */
        map = new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTORY, true) {
 @Override
 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
 return size > MAX_CACHE_SIZE;
 }
        };
    }

    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    public synchronized V get(K key) {
        return map.get(key);
    }

    public synchronized void remove(K key) {
        map.remove(key);
    }

    public synchronized Set<Map.Entry<K, V>> getAll {
        return map.entrySet;
    }

    @Override
    public String toString {
        StringBuilder stringBuilder = new StringBuilder;
        for (Map.Entry<K, V> entry : map.entrySet) {
 stringBuilder.append(String.format("%s: %s  ", entry.getKey, entry.getValue));
        }
        return stringBuilder.toString;
    }

    public static void main(String[] args) {
        LRU1<Integer, Integer> lru1 = new LRU1<>(5);
        lru1.put(1, 1);
        lru1.put(2, 2);
        lru1.put(3, 3);
        System.out.println(lru1);
        lru1.get(1);
        System.out.println(lru1);
        lru1.put(4, 4);
        lru1.put(5, 5);
        lru1.put(6, 6);
        System.out.println(lru1);
    }
}

運行結果:

從運行結果中可以看出,實現了LRU緩存的思想。

接著使用HashMap和鏈表來實現LRU緩存。

主要的思想和上述基本一致,每次添加元素或者讀取元素就將元素放置在鏈表的頭,當緩存滿了之後,就可以將尾結點元素刪除,這樣就實現了LRU緩存。

這種方法中是通過自己編寫代碼移動結點和刪除結點,為了防止緩存大小超過限制,每次進行put的時候都會進行檢查,若緩存滿了則刪除尾部元素。

import java.util.HashMap;

/**
 * 使用cache和鏈表實現緩存
 */
public class LRU2<K, V> {
    private final int MAX_CACHE_SIZE;
    private Entry<K, V> head;
    private Entry<K, V> tail;

    private HashMap<K, Entry<K, V>> cache;

    public LRU2(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;
        cache = new HashMap<>;
    }

    public void put(K key, V value) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) {
 if (cache.size >= MAX_CACHE_SIZE) {
 cache.remove(tail.key);
 removeTail;
 }
        }
        entry = new Entry<>;
        entry.key = key;
        entry.value = value;
        moveToHead(entry);
        cache.put(key, entry);
    }

    public V get(K key) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) {
 return null;
        }
        moveToHead(entry);
        return entry.value;
    }

    public void remove(K key) {
        Entry<K, V> entry = getEntry(key);
        if (entry != null) {
 if (entry == head) {
 Entry<K, V> next = head.next;
 head.next = null;
 head = next;
 head.pre = null;
 } else if (entry == tail) {
 Entry<K, V> prev = tail.pre;
 tail.pre = null;
 tail = prev;
 tail.next = null;
 } else {
 entry.pre.next = entry.next;
 entry.next.pre = entry.pre;
 }
 cache.remove(key);
        }
    }

    private void removeTail {
        if (tail != null) {
 Entry<K, V> prev = tail.pre;
 if (prev == null) {
 head = null;
 tail = null;
 } else {
 tail.pre = null;
 tail = prev;
 tail.next = null;
 }
        }
    }

    private void moveToHead(Entry<K, V> entry) {
        if (entry == head) {
 return;
        }
        if (entry.pre != null) {
 entry.pre.next = entry.next;
        }
        if (entry.next != null) {
 entry.next.pre = entry.pre;
        }
        if (entry == tail) {
 Entry<K, V> prev = entry.pre;
 if (prev != null) {
 tail.pre = null;
 tail = prev;
 tail.next = null;
 }
        }

        if (head == null || tail == null) {
 head = tail = entry;
 return;
        }

        entry.next = head;
        head.pre = entry;
        entry.pre = null;
        head = entry;
    }

    private Entry<K, V> getEntry(K key) {
        return cache.get(key);
    }

    private static class Entry<K, V> {
        Entry<K, V> pre;
        Entry<K, V> next;
        K key;
        V value;
    }

    @Override
    public String toString {
        StringBuilder stringBuilder = new StringBuilder;
        Entry<K, V> entry = head;
        while (entry != null) {
 stringBuilder.append(String.format("%s:%s ", entry.key, entry.value));
 entry = entry.next;
        }
        return stringBuilder.toString;
    }

    public static void main(String[] args) {
        LRU2<Integer, Integer> lru2 = new LRU2<>(5);
        lru2.put(1, 1);
        System.out.println(lru2);
        lru2.put(2, 2);
        System.out.println(lru2);
        lru2.put(3, 3);
        System.out.println(lru2);
        lru2.get(1);
        System.out.println(lru2);
        lru2.put(4, 4);
        lru2.put(5, 5);
        lru2.put(6, 6);
        System.out.println(lru2);
    }
}

運行結果:

FIFO

FIFO就是先進先出,可以使用LinkedHashMap進行實現。 當第三個參數傳入為false或者是默認的時候,就可以實現按插入順序排序,就可以實現FIFO緩存了。

實現代碼跟上述使用LinkedHashMap實現LRU的代碼基本一致,主要就是構造函數的傳值有些不同。

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class LRU1<K, V> {
    private final int MAX_CACHE_SIZE;
    private final float DEFAULT_LOAD_FACTORY = 0.75f;

    LinkedHashMap<K, V> map;

    public LRU1(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;
        int capacity = (int)Math.ceil(MAX_CACHE_SIZE / DEFAULT_LOAD_FACTORY) + 1;
        /*
         * 第三個參數設置為true,代表linkedlist按訪問順序排序,可作為LRU緩存
         * 第三個參數設置為false,代表按插入順序排序,可作為FIFO緩存
         */
        map = new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTORY, false) {
 @Override
 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
 return size > MAX_CACHE_SIZE;
 }
        };
    }

    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    public synchronized V get(K key) {
        return map.get(key);
    }

    public synchronized void remove(K key) {
        map.remove(key);
    }

    public synchronized Set<Map.Entry<K, V>> getAll {
        return map.entrySet;
    }

    @Override
    public String toString {
        StringBuilder stringBuilder = new StringBuilder;
        for (Map.Entry<K, V> entry : map.entrySet) {
 stringBuilder.append(String.format("%s: %s  ", entry.getKey, entry.getValue));
        }
        return stringBuilder.toString;
    }

    public static void main(String[] args) {
        LRU1<Integer, Integer> lru1 = new LRU1<>(5);
        lru1.put(1, 1);
        lru1.put(2, 2);
        lru1.put(3, 3);
        System.out.println(lru1);
        lru1.get(1);
        System.out.println(lru1);
        lru1.put(4, 4);
        lru1.put(5, 5);
        lru1.put(6, 6);
        System.out.println(lru1);
    }
}

運行結果:

以上就是使用Java實現這兩種緩存的方式,從中可以看出,LinkedHashMap實現緩存較為容易,因為底層函數對此已經有了支持,自己編寫鏈表實現LRU緩存也是借鑑了LinkedHashMap中實現的思想。在Java中不只是這兩種數據結構可以實現緩存,比如ConcurrentHashMap、WeakHashMap在某些場景下也是可以作為緩存的,到底用哪一種數據結構主要是看場景再進行選擇,但是很多思想都是可以通用的。

相關推薦

推薦中...