從零開始手寫 redis(七)LRU 快取淘汰策略詳解
前言
java從零手寫實現redis(一)如何實現固定大小的快取?
java從零手寫實現redis(三)redis expire 過期原理
java從零手寫實現redis(三)記憶體資料如何重啟不丟失?
java從零手寫實現redis(五)過期策略的另一種實現思路
java從零手寫實現redis(六)AOF 持久化原理詳解及實現
我們前面簡單實現了 redis 的幾個特性,java從零手寫實現redis(一)如何實現固定大小的快取? 中實現了先進先出的驅除策略。
但是實際工作實踐中,一般推薦使用 LRU/LFU 的驅除策略。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-CDmJtApe-1601733022771)(https://cdn.pixabay.com/photo/2018/12/01/18/12/box-3849936_1280.jpg)]
LRU 基礎知識
擴充學習
LRU 是什麼
LRU 是由 Least Recently Used 的首字母組成,表示最近最少使用的含義,一般使用在物件淘汰演算法上。
也是比較常見的一種淘汰演算法。
其核心思想是如果資料最近被訪問過,那麼將來被訪問的機率也更高。
連續性
在電腦科學中,有一個指導準則:連續性準則。
時間連續性:對於資訊的訪問,最近被訪問過,被再次訪問的可能性會很高。快取就是基於這個理念進行資料淘汰的。
空間連續性:對於磁碟資訊的訪問,將很有可能訪問連續的空間資訊。所以會有 page 預取來提升效能。
實現步驟
-
新資料插入到連結串列頭部;
-
每當快取命中(即快取資料被訪問),則將資料移到連結串列頭部;
-
當連結串列滿的時候,將連結串列尾部的資料丟棄。
其實比較簡單,比起 FIFO 的佇列,我們引入一個連結串列實現即可。
一點思考
我們針對上面的 3 句話,逐句考慮一下,看看有沒有值得優化點或者一些坑。
如何判斷是新資料?
(1) 新資料插入到連結串列頭部;
我們使用的是連結串列。
判斷新資料最簡單的方法就是遍歷是否存在,對於連結串列,這是一個 O(n) 的時間複雜度。
其實效能還是比較差的。
當然也可以考慮空間換時間,比如引入一個 set 之類的,不過這樣對空間的壓力會加倍。
什麼是快取命中
(2)每當快取命中(即快取資料被訪問),則將資料移到連結串列頭部;
put(key,value) 的情況,就是新元素。如果已有這個元素,可以先刪除,再加入,參考上面的處理。
get(key) 的情況,對於元素訪問,刪除已有的元素,將新元素放在頭部。
remove(key) 移除一個元素,直接刪除已有元素。
keySet() valueSet() entrySet() 這些屬於無差別訪問,我們不對佇列做調整。
移除
(3)當連結串列滿的時候,將連結串列尾部的資料丟棄。
連結串列滿只有一種場景,那就是新增元素的時候,也就是執行 put(key, value) 的時候。
直接刪除對應的 key 即可。
java 程式碼實現
介面定義
和 FIFO 的介面保持一致,呼叫地方也不變。
為了後續 LRU/LFU 實現,新增 remove/update 兩個方法。
public interface ICacheEvict<K, V> {
/**
* 驅除策略
*
* @param context 上下文
* @since 0.0.2
* @return 是否執行驅除
*/
boolean evict(final ICacheEvictContext<K, V> context);
/**
* 更新 key 資訊
* @param key key
* @since 0.0.11
*/
void update(final K key);
/**
* 刪除 key 資訊
* @param key key
* @since 0.0.11
*/
void remove(final K key);
}
LRU 實現
直接基於 LinkedList 實現:
/**
* 丟棄策略-LRU 最近最少使用
* @author binbin.hou
* @since 0.0.11
*/
public class CacheEvictLRU<K,V> implements ICacheEvict<K,V> {
private static final Log log = LogFactory.getLog(CacheEvictLRU.class);
/**
* list 資訊
* @since 0.0.11
*/
private final List<K> list = new LinkedList<>();
@Override
public boolean evict(ICacheEvictContext<K, V> context) {
boolean result = false;
final ICache<K,V> cache = context.cache();
// 超過限制,移除隊尾的元素
if(cache.size() >= context.size()) {
K evictKey = list.get(list.size()-1);
// 移除對應的元素
cache.remove(evictKey);
result = true;
}
return result;
}
/**
* 放入元素
* (1)刪除已經存在的
* (2)新元素放到元素頭部
*
* @param key 元素
* @since 0.0.11
*/
@Override
public void update(final K key) {
this.list.remove(key);
this.list.add(0, key);
}
/**
* 移除元素
* @param key 元素
* @since 0.0.11
*/
@Override
public void remove(final K key) {
this.list.remove(key);
}
}
實現比較簡單,相對 FIFO 多了三個方法:
update():我們做一點簡化,認為只要是訪問,就是刪除,然後插入到隊首。
remove():刪除就是直接刪除。
這三個方法是用來更新最近使用情況的。
那什麼時候呼叫呢?
註解屬性
為了保證核心流程,我們基於註解實現。
新增屬性:
/**
* 是否執行驅除更新
*
* 主要用於 LRU/LFU 等驅除策略
* @return 是否
* @since 0.0.11
*/
boolean evict() default false;
註解使用
有哪些方法需要使用?
@Override
@CacheInterceptor(refresh = true, evict = true)
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
@CacheInterceptor(evict = true)
@SuppressWarnings("unchecked")
public V get(Object key) {
//1. 重新整理所有過期資訊
K genericKey = (K) key;
this.expire.refreshExpire(Collections.singletonList(genericKey));
return map.get(key);
}
@Override
@CacheInterceptor(aof = true, evict = true)
public V put(K key, V value) {
//...
}
@Override
@CacheInterceptor(aof = true, evict = true)
public V remove(Object key) {
return map.remove(key);
}
註解驅除攔截器實現
執行順序:放在方法之後更新,不然每次當前操作的 key 都會被放在最前面。
/**
* 驅除策略攔截器
*
* @author binbin.hou
* @since 0.0.11
*/
public class CacheInterceptorEvict<K,V> implements ICacheInterceptor<K, V> {
private static final Log log = LogFactory.getLog(CacheInterceptorEvict.class);
@Override
public void before(ICacheInterceptorContext<K,V> context) {
}
@Override
@SuppressWarnings("all")
public void after(ICacheInterceptorContext<K,V> context) {
ICacheEvict<K,V> evict = context.cache().evict();
Method method = context.method();
final K key = (K) context.params()[0];
if("remove".equals(method.getName())) {
evict.remove(key);
} else {
evict.update(key);
}
}
}
我們只對 remove 方法做下特判,其他方法都使用 update 更新資訊。
引數直接取第一個引數。
測試
ICache<String, String> cache = CacheBs.<String,String>newInstance()
.size(3)
.evict(CacheEvicts.<String, String>lru())
.build();
cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO");
// 訪問一次A
cache.get("A");
cache.put("D", "LRU");
Assert.assertEquals(3, cache.size());
System.out.println(cache.keySet());
- 日誌資訊
[D, A, C]
通過 removeListener 日誌也可以看到 B 被移除了:
[DEBUG] [2020-10-02 21:33:44.578] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
小結
redis LRU 淘汰策略,實際上並不是真正的 LRU。
LRU 有一個比較大的問題,就是每次 O(n) 去查詢,這個在 keys 數量特別多的時候,還是很慢的。
如果 redis 這麼設計肯定慢的要死了。
個人的理解是可以用空間換取時間,比如新增一個 Map<String, Integer>
儲存在 list 中的 keys 和下標,O(1) 的速度去查詢,但是空間複雜度翻倍了。
不過這個犧牲還是值得的。這種後續統一做下優化,將各種優化點統一考慮,這樣可以統籌全域性,也便於後期統一調整。
下一節我們將一起來實現以下改進版的 LRU。
Redis 做的事情,就是將看起來的簡單的事情,做到一種極致,這一點值得每一個開源軟體學習。
文中主要講述了思路,實現部分因為篇幅限制,沒有全部貼出來。
覺得本文對你有幫助的話,歡迎點贊評論收藏關注一波~
你的鼓勵,是我最大的動力~
相關文章
- 從零開始手寫 redis(八)樸素 LRU 淘汰演算法效能優化Redis演算法優化
- 【redis前傳】自己手寫一個LRU策略 | redis淘汰策略Redis
- java 從零開始手寫 redis(十)快取淘汰演算法 LFU 最少使用頻次JavaRedis快取演算法
- Redis篇:持久化、淘汰策略,快取失效策略Redis持久化快取
- 配置Redis作為快取(六種淘汰策略)Redis快取
- 什麼是LRU快取淘汰機制快取
- Go 語言手寫本地 LRU 快取Go快取
- Redis(二十):Redis資料過期和淘汰策略詳解(轉)Redis
- 從零開始手寫Koa2框架框架
- LRU工程實現原始碼(一):Redis 記憶體淘汰策略原始碼Redis記憶體
- Redis淘汰策略Redis
- LRU 快取淘汰演算法的兩種實現快取演算法
- Redis-6-三種快取讀寫策略Redis快取
- Redis 的快取淘汰機制(Eviction)Redis快取
- 【React技術棧】從零開始手寫reduxReactRedux
- Redis詳解(十一)------ 過期刪除策略和記憶體淘汰策略Redis記憶體
- 深度詳解GaussDB bufferpool快取策略快取
- Redis詳解(十二)------ 快取穿透、快取擊穿、快取雪崩Redis快取穿透
- java 從零開始手寫 RPC (04) -序列化JavaRPC
- (四)Redis 快取應用、淘汰機制Redis快取
- 從零開始學Electron筆記(七)筆記
- 手把手使用 PHP 實現 LRU 快取淘汰演算法PHP快取演算法
- 從零搭建Spring Boot腳手架(6):整合Redis作為快取Spring BootRedis快取
- LRU快取替換策略及C#實現快取C#
- 前端進階演算法3:從瀏覽器快取淘汰策略和Vue的keep-alive學習LRU演算法前端演算法瀏覽器快取VueKeep-Alive
- 從零開始手寫一個微前端框架-渲染篇前端框架
- 從零開始寫一個ExporterExport
- 從零開始寫 Docker(七)---實現 mydocker commit 打包容器成映象DockerMIT
- 從零開始搭建腳手架
- java 從零開始手寫 RPC (01) 基於 websocket 實現JavaRPCWeb
- 用Java寫一個分散式快取——快取淘汰演算法Java分散式快取演算法
- 從零開始編寫指令碼引擎指令碼
- 看動畫理解「連結串列」實現LRU快取淘汰演算法動畫快取演算法
- 從零開始仿寫一個抖音App——開始APP
- 從零開始學五筆(七):折區字根
- Redis之key的淘汰策略Redis
- 146. LRU 快取快取
- LRU快取機制快取