緩存淘汰算法
在高并發(fā)耻瑟、高性能的質(zhì)量要求不斷提高時旨指,我們首先會想到的就是利用緩存予以應對。
第一次請求時把計算好的結(jié)果存放在緩存中喳整,下次遇到同樣的請求時谆构,把之前保存在緩存中的數(shù)據(jù)直接拿來使用。
但是框都,緩存的空間一般都是有限搬素,不可能把所有的結(jié)果全部保存下來。那么魏保,當緩存空間全部被占滿再有新的數(shù)據(jù)需要被保存熬尺,就要決定刪除原來的哪些數(shù)據(jù)。如何做這樣決定需要使用緩存淘汰算法谓罗。
常用的緩存淘汰算法有:FIFO粱哼、LRU、LFU檩咱,下面我們就逐一介紹一下揭措。
FIFO
FIFO,F(xiàn)irst In First Out刻蚯,先進先出算法蜂筹。判斷被存儲的時間,離目前最遠的數(shù)據(jù)優(yōu)先被淘汰芦倒。簡單地說艺挪,先存入緩存的數(shù)據(jù),先被淘汰。
最早存入緩存的數(shù)據(jù)麻裳,其不再被使用的可能性比剛存入緩存的可能性大口蝠。建立一個FIFO隊列,記錄所有在緩存中的數(shù)據(jù)津坑。當一條數(shù)據(jù)被存入緩存時妙蔗,就把它插在隊尾上。需要被淘汰的數(shù)據(jù)一直在隊列頭疆瑰。這種算法只是在按線性順序訪問數(shù)據(jù)時才是理想的眉反,否則效率不高。因為那些常被訪問的數(shù)據(jù)穆役,往往在緩存中也停留得最久寸五,結(jié)果它們卻因變“老”而不得不被淘汰出去。
FIFO算法用隊列實現(xiàn)就可以了耿币,這里就不做代碼實現(xiàn)了梳杏。
LRU
LRU,Least Recently Used淹接,最近最少使用算法十性。判斷最近被使用的時間,目前最遠的數(shù)據(jù)優(yōu)先被淘汰塑悼。簡單地說劲适,LRU 的淘汰規(guī)則是基于訪問時間。
如果一個數(shù)據(jù)在最近一段時間沒有被使用到厢蒜,那么可以認為在將來它被使用的可能性也很小霞势。因此,當緩存空間滿時郭怪,最久沒有使用的數(shù)據(jù)最先被淘汰支示。
在Java中,其實LinkedHashMap已經(jīng)實現(xiàn)了LRU緩存淘汰算法鄙才,需要在構(gòu)造函數(shù)第三個參數(shù)傳入true颂鸿,表示按照時間順序訪問≡茆郑可以直接繼承LinkedHashMap來實現(xiàn)嘴纺。
package one.more;
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache<K, V> extends LinkedHashMap<K, V> {
/**
* 容量限制
*/
private int capacity;
LruCache(int capacity) {
// 初始大小,0.75是裝載因子浓冒,true是表示按照訪問時間排序
super(capacity, 0.75f, true);
//緩存最大容量
this.capacity = capacity;
}
/**
* 重寫removeEldestEntry方法栽渴,如果緩存滿了,則把鏈表頭部第一個節(jié)點和對應的數(shù)據(jù)刪除稳懒。
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
我寫一個簡單的程序測試一下:
package one.more;
public class TestApp {
public static void main(String[] args) {
LruCache<String, String> cache = new LruCache(3);
cache.put("keyA", "valueA");
System.out.println("put keyA");
System.out.println(cache);
System.out.println("=========================");
cache.put("keyB", "valueB");
System.out.println("put keyB");
System.out.println(cache);
System.out.println("=========================");
cache.put("keyC", "valueC");
System.out.println("put keyC");
System.out.println(cache);
System.out.println("=========================");
cache.get("keyA");
System.out.println("get keyA");
System.out.println(cache);
System.out.println("=========================");
cache.put("keyD", "valueD");
System.out.println("put keyD");
System.out.println(cache);
}
}
運行結(jié)果如下:
put keyA
{keyA=valueA}
=========================
put keyB
{keyA=valueA, keyB=valueB}
=========================
put keyC
{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
{keyC=valueC, keyA=valueA, keyD=valueD}
當然闲擦,這個不是面試官想要的,也不是我們想要的。我們可以使用雙向鏈表和哈希表進行實現(xiàn)墅冷,哈希表用于存儲對應的數(shù)據(jù)纯路,雙向鏈表用于數(shù)據(jù)被使用的時間先后順序。
在訪問數(shù)據(jù)時寞忿,如果數(shù)據(jù)已存在緩存中驰唬,則把該數(shù)據(jù)的對應節(jié)點移到鏈表尾部。如此操作腔彰,在鏈表頭部的節(jié)點則是最近最少使用的數(shù)據(jù)叫编。
當需要添加新的數(shù)據(jù)到緩存時,如果該數(shù)據(jù)已存在緩存中霹抛,則把該數(shù)據(jù)對應的節(jié)點移到鏈表尾部搓逾;如果不存在,則新建一個對應的節(jié)點上炎,放到鏈表尾部恃逻;如果緩存滿了雏搂,則把鏈表頭部第一個節(jié)點和對應的數(shù)據(jù)刪除藕施。
package one.more;
import java.util.HashMap;
import java.util.Map;
public class LruCache<K, V> {
/**
* 頭結(jié)點
*/
private Node head;
/**
* 尾結(jié)點
*/
private Node tail;
/**
* 容量限制
*/
private int capacity;
/**
* key和數(shù)據(jù)的映射
*/
private Map<K, Node> map;
LruCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>();
}
public V put(K key, V value) {
Node node = map.get(key);
// 數(shù)據(jù)存在,將節(jié)點移動到隊尾
if (node != null) {
V oldValue = node.value;
//更新數(shù)據(jù)
node.value = value;
moveToTail(node);
return oldValue;
} else {
Node newNode = new Node(key, value);
// 數(shù)據(jù)不存在凸郑,判斷鏈表是否滿
if (map.size() == capacity) {
// 如果滿裳食,則刪除隊首節(jié)點,更新哈希表
map.remove(removeHead().key);
}
// 放入隊尾節(jié)點
addToTail(newNode);
map.put(key, newNode);
return null;
}
}
public V get(K key) {
Node node = map.get(key);
if (node != null) {
moveToTail(node);
return node.value;
}
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LruCache{");
Node curr = this.head;
while (curr != null) {
if(curr != this.head){
sb.append(',').append(' ');
}
sb.append(curr.key);
sb.append('=');
sb.append(curr.value);
curr = curr.next;
}
return sb.append('}').toString();
}
private void addToTail(Node newNode) {
if (newNode == null) {
return;
}
if (head == null) {
head = newNode;
tail = newNode;
} else {
//連接新節(jié)點
tail.next = newNode;
newNode.pre = tail;
//更新尾節(jié)點指針為新節(jié)點
tail = newNode;
}
}
private void moveToTail(Node node) {
if (tail == node) {
return;
}
if (head == node) {
head = node.next;
head.pre = null;
} else {
//調(diào)整雙向鏈表指針
node.pre.next = node.next;
node.next.pre = node.pre;
}
node.pre = tail;
node.next = null;
tail.next = node;
tail = node;
}
private Node removeHead() {
if (head == null) {
return null;
}
Node res = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = res.next;
head.pre = null;
res.next = null;
}
return res;
}
class Node {
K key;
V value;
Node pre;
Node next;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}
再次運行測試程序芙沥,結(jié)果如下:
put keyA
LruCache{keyA=valueA}
=========================
put keyB
LruCache{keyA=valueA, keyB=valueB}
=========================
put keyC
LruCache{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
LruCache{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
LruCache{keyC=valueC, keyA=valueA, keyD=valueD}
LFU
LFU诲祸,Least Frequently Used,最不經(jīng)常使用算法而昨,在一段時間內(nèi)救氯,數(shù)據(jù)被使用次數(shù)最少的,優(yōu)先被淘汰歌憨。簡單地說着憨,LFU 的淘汰規(guī)則是基于訪問次數(shù)。
如果一個數(shù)據(jù)在最近一段時間很少被使用到务嫡,那么可以認為在將來它被使用的可能性也很小甲抖。因此,當空間滿時心铃,最小頻率使用的數(shù)據(jù)最先被淘汰准谚。
我們可以使用雙哈希表進行實現(xiàn),一個哈希表用于存儲對應的數(shù)據(jù)去扣,另一個哈希表用于存儲數(shù)據(jù)被使用次數(shù)和對應的數(shù)據(jù)柱衔。
package one.more;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class LfuCache<K, V> {
/**
* 容量限制
*/
private int capacity;
/**
* 當前最小使用次數(shù)
*/
private int minUsedCount;
/**
* key和數(shù)據(jù)的映射
*/
private Map<K, Node> map;
/**
* 數(shù)據(jù)頻率和對應數(shù)據(jù)組成的鏈表
*/
private Map<Integer, List<Node>> usedCountMap;
public LfuCache(int capacity) {
this.capacity = capacity;
this.minUsedCount = 1;
this.map = new HashMap<>();
this.usedCountMap = new HashMap<>();
}
public V get(K key) {
Node node = map.get(key);
if (node == null) {
return null;
}
// 增加數(shù)據(jù)的訪問頻率
addUsedCount(node);
return node.value;
}
public V put(K key, V value) {
Node node = map.get(key);
if (node != null) {
// 如果存在則增加該數(shù)據(jù)的訪問頻次
V oldValue = node.value;
node.value = value;
addUsedCount(node);
return oldValue;
} else {
// 數(shù)據(jù)不存在,判斷鏈表是否滿
if (map.size() == capacity) {
// 如果滿,則刪除隊首節(jié)點唆铐,更新哈希表
List<Node> list = usedCountMap.get(minUsedCount);
Node delNode = list.get(0);
list.remove(delNode);
map.remove(delNode.key);
}
// 新增數(shù)據(jù)并放到數(shù)據(jù)頻率為1的數(shù)據(jù)鏈表中
Node newNode = new Node(key, value);
map.put(key, newNode);
List<Node> list = usedCountMap.get(1);
if (list == null) {
list = new LinkedList<>();
usedCountMap.put(1, list);
}
list.add(newNode);
minUsedCount = 1;
return null;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LfuCache{");
List<Integer> usedCountList = this.usedCountMap.keySet().stream().collect(Collectors.toList());
usedCountList.sort(Comparator.comparingInt(i -> i));
int count = 0;
for (int usedCount : usedCountList) {
List<Node> list = this.usedCountMap.get(usedCount);
if (list == null) {
continue;
}
for (Node node : list) {
if (count > 0) {
sb.append(',').append(' ');
}
sb.append(node.key);
sb.append('=');
sb.append(node.value);
sb.append("(UsedCount:");
sb.append(node.usedCount);
sb.append(')');
count++;
}
}
return sb.append('}').toString();
}
private void addUsedCount(Node node) {
List<Node> oldList = usedCountMap.get(node.usedCount);
oldList.remove(node);
// 更新最小數(shù)據(jù)頻率
if (minUsedCount == node.usedCount && oldList.isEmpty()) {
minUsedCount++;
}
node.usedCount++;
List<Node> set = usedCountMap.get(node.usedCount);
if (set == null) {
set = new LinkedList<>();
usedCountMap.put(node.usedCount, set);
}
set.add(node);
}
class Node {
K key;
V value;
int usedCount = 1;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
再次運行測試程序捶码,結(jié)果如下:
put keyA
LfuCache{keyA=valueA(UsedCount:1)}
=========================
put keyB
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1)}
=========================
put keyC
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1)}
=========================
get keyA
LfuCache{keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1), keyA=valueA(UsedCount:2)}
=========================
put keyD
LfuCache{keyC=valueC(UsedCount:1), keyD=valueD(UsedCount:1), keyA=valueA(UsedCount:2)}
總結(jié)
看到這里,你已經(jīng)超越了大多數(shù)人或链!
FIFO惫恼,F(xiàn)irst In First Out,先進先出算法澳盐。判斷被存儲的時間祈纯,離目前最遠的數(shù)據(jù)優(yōu)先被淘汰,可以使用隊列實現(xiàn)叼耙。
LRU腕窥,Least Recently Used,最近最少使用算法筛婉。判斷最近被使用的時間簇爆,目前最遠的數(shù)據(jù)優(yōu)先被淘汰,可以使用雙向鏈表和哈希表實現(xiàn)爽撒。
LFU入蛆,Least Frequently Used,最不經(jīng)常使用算法硕勿,在一段時間內(nèi)哨毁,數(shù)據(jù)被使用次數(shù)最少的,優(yōu)先被淘汰源武,可以使用雙哈希表實現(xiàn)扼褪。