java從零手寫實現redis(一)如何實現固定大小的緩存?java
java從零手寫實現redis(三)redis expire 過時原理git
java從零手寫實現redis(三)內存數據如何重啓不丟失?github
java從零手寫實現redis(五)過時策略的另外一種實現思路算法
java從零手寫實現redis(六)AOF 持久化原理詳解及實現緩存
咱們前面簡單實現了 redis 的幾個特性,java從零手寫實現redis(一)如何實現固定大小的緩存? 中實現了先進先出的驅除策略。ide
可是實際工做實踐中,通常推薦使用 LRU/LFU 的驅除策略。性能
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 便可。
和 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); }
直接基於 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 作的事情,就是將看起來的簡單的事情,作到一種極致,這一點值得每個開源軟件學習。
文中主要講述了思路,實現部分由於篇幅限制,沒有所有貼出來。
開源地址: https://github.com/houbb/cache
以爲本文對你有幫助的話,歡迎點贊評論收藏關注一波~
你的鼓勵,是我最大的動力~