其實吧,LRU也就那麼回事。

這是why哥的第 81 篇原創文章git

你面試的時候碰見過LRU嗎?

LRU 算法,全稱是Least Recently Used。github

翻譯過來就是最近最少使用算法。面試

這個算法的思想就是:若是一個數據在最近一段時間沒有被訪問到,那麼在未來它被訪問的可能性也很小。因此,當指定的空間已存滿數據時,應當把最久沒有被訪問到的數據淘汰。redis

聽描述你也知道了,它是一種淘汰算法。算法

這個算法也是面試的一個高頻考點。sql

有的面試官甚至要求手擼一個 LRU 算法出來。數據庫

其實我以爲吧,遇到這種狀況也不要慌,你就按照本身的思路寫一個出來就行。數組

賭一把,面試官也許本身短期內都手擼不出來一個無 bug 的 LRU。他也只是檢查幾個關鍵點、看看你的代碼風格、觀察一下你的解題思路而已。緩存

但其實大多數狀況下面試場景都是這樣的:數據結構

面試官:你知道 LRU 算法嗎?

我:知道,翻譯過來就是最近最少使用算法。其思想是(前面說過,就不復述了)......

面試官:那你能給我談談你有哪些方法來實現 LRU 算法呢?

這個時候問的是什麼?

問的是:咱們都知道這個算法的思路了,請你按照這個思路給出一個能夠落地的解決方案。

不用徒手擼一個。

方案一:數組

若是以前徹底沒有接觸過 LRU 算法,僅僅知道其思路。

第一次聽就要求你給一個實現方案,那麼數組的方案應該是最容易想到的。

假設咱們有一個定長數組。數組中的元素都有一個標記。這個標記能夠是時間戳,也能夠是一個自增的數字。

假設咱們用自增的數字。

每放入一個元素,就把數組中已經存在的數據的標記更新一下,進行自增。當數組滿了後,就將數字最大的元素刪除掉。

每訪問一個元素,就將被訪問的元素的數字置爲 0 。

這不就是 LRU 算法的一個實現方案嗎?

按照這個思路,擼一份七七八八的代碼出來,問題應該不大吧?

可是這一種方案的弊端也是很明顯:須要不停地維護數組中元素的標記。

那麼你以爲它的時間複雜度是多少?

是的,每次操做都伴隨着一次遍歷數組修改標記的操做,因此時間複雜度是O(n)。

可是這個方案,面試官確定是不會滿意的。由於,這不是他心中的標準答案。

也許他都沒想過:你還能給出這種方案呢?

可是它不會說出來,只會輕輕的說一句:還有其餘的方案嗎?

方案二:鏈表

因而你扣着腦袋想了想。最近最少使用,感受是須要一個有序的結構。

我每插入一個元素的時候,就追加在數組的末尾。

我每訪問一次元素,就把被訪問的元素移動到數組的末尾。

這樣最近被用的必定是在最後面的,頭部的就是最近最少使用的。

當指定長度被用完了以後,就把頭部元素移除掉就好了。

這是個什麼結構?

這不就是個鏈表嗎?

維護一個有序單鏈表,越靠近鏈表頭部的結點是越早以前訪問的。

當有一個新的數據被訪問時,咱們從鏈表頭部開始順序遍歷鏈表。

若是此數據以前已經被緩存在鏈表中了,咱們遍歷獲得這個數據的對應結點,並將其從原來的位置刪除,並插入到鏈表尾部。

若是此數據沒在緩存鏈表中,怎麼辦?

分兩種狀況:

  • 若是此時緩存未滿,可直接在鏈表尾部插入新節點存儲此數據;
  • 若是此時緩存已滿,則刪除鏈表頭部節點,再在鏈表尾部插入新節點。

你看,這不又是 LRU 算法的一個實現方案嗎?

按照這個思路,擼一份八九不離十的代碼出來,問題應該不大吧?

這個方案比數組的方案好在哪裏呢?

我以爲就是莫名其妙的高級感,就是看起來就比數組高級了一點。

從時間複雜度的角度看,由於鏈表插入、查詢的時候都要遍歷鏈表,查看數據是否存在,因此它仍是O(n)。

總之,這也不是面試官想要的答案。

當你回答出這個方案以後,面試官也許會說:你能不能給我一個查詢和插入的時間複雜度都是O(1)的解決方案?

到這裏,就得看天分了。

有一說一,若是我以前徹底沒有接觸過 LRU 算法,我能夠很是自信的說:

方案三:雙向鏈表+哈希表。

若是咱們想要查詢和插入的時間複雜度都是O(1),那麼咱們須要一個知足下面三個條件的數據結構:

  • 1.首先這個數據結構必須是有時序的,以區分最近使用的和好久沒有使用的數據,當容量滿了以後,要刪除最久未使用的那個元素。
  • 2.要在這個數據結構中快速找到某個 key 是否存在,並返回其對應的 value。
  • 3.每次訪問這個數據結構中的某個 key,須要將這個元素變爲最近使用的。也就是說,這個數據結構要支持在任意位置快速插入和刪除元素。

那麼,你說什麼樣的數據結構同時符合上面的條件呢?

查找快,咱們能想到哈希表。可是哈希表的數據是亂序的。

有序,咱們能想到鏈表,插入、刪除都很快,可是查詢慢。

因此,咱們得讓哈希表和鏈表結合一下,成長一下,造成一個新的數據結構,那就是:哈希鏈表,LinkedHashMap。

這個結構大概長這樣:

藉助這個結構,咱們再來分析一下上面的三個條件:

  • 1.若是每次默認從鏈表尾部添加元素,那麼顯然越靠近尾部的元素就越是最近使用的。越靠近頭部的元素就是越久未使用的。
  • 2.對於某一個 key ,能夠經過哈希錶快速定位到鏈表中的節點,從而取得對應的 value。
  • 3.鏈表顯然是支持在任意位置快速插入和刪除的,修改指針就行。可是單鏈表沒法按照索引快速訪問某一個位置的元素,都是須要遍歷鏈表的,因此這裏藉助哈希表,能夠經過 key,快速的映射到任意一個鏈表節點,而後進行插入和刪除。

這纔是面試官想要關於 LRU 的正確答案。

可是你覺得回答到這裏就結束了嗎?

面試官爲了確認你的掌握程度,還會追問一下。

那麼請問:爲何這裏要用雙鏈表呢,單鏈表爲何不行?

你內心一慌:我靠,這題我也背過。一時想不起來了。

因此,別隻顧着背答案,得理解。

你想啊,咱們是否是涉及到刪除元素的操做?

那麼鏈表刪除元素除了本身自己的指針信息,還須要什麼東西?

是否是還須要前驅節點的指針?

那麼咱們這裏要求時間複雜度是O(1),因此怎麼才能直接獲取到前驅節點的指針?

這玩意是否是就得上雙鏈表?

咦,你看在一波靈魂追問中,就獲得了答案。

面試官的第二個問題又隨之而來了:哈希表裏面已經保存了 key ,那麼鏈表中爲何還要存儲 key 和 value 呢,只存入 value 不就好了?

不會也不要慌,你先分析一波。

剛剛咱們說刪除鏈表中的節點,須要藉助雙鏈表來實現O(1)。

刪除了鏈表中的節點,而後呢?

是否是還忘記了什麼東西?

是否是還有一個哈希表忘記操做了?

哈希表是否是也得進行對應的刪除操做?

刪除哈希表須要什麼東西?

是否是須要 key,才能刪除對應的 value?

這個 key 從哪裏來?

是否是隻能從鏈表中的結點裏面來?

若是鏈表中的結點,只有 value 沒有 key,那麼咱們就沒法刪除哈希表的 key。那不就完犢子了嗎?

又是一波靈魂追問。

因此,你如今知道答案了嗎?

另外在多說一句,有的小夥伴可能會直接回答藉助 LinkedHashMap 來實現。

我以爲吧,你要是實在不知道,也能夠這樣說。

可是,這個回答多是面試官最不想聽到的回答了。

他會以爲你投機取巧。

可是呢,實際開發中,真正要用的時候,咱們仍是用的 LinkedHashMap。

你說這個事情,難受不難受。

好了,你覺得到這裏面試就結束了?

LRU 在 MySQL 中的應用

面試官:小夥子剛剛 LRU 回答的不錯哈。要不你給我講講,LRU 在 MySQL 中的應用?

LRU 在 MySQL 的應用就是 Buffer Pool,也就是緩衝池。

它的目的是爲了減小磁盤 IO。

緩衝池具體是幹啥的,我這裏就不展開說了。

你就知道它是一塊連續的內存,默認大小 128M,能夠進行修改。

這一塊連續的內存,被劃分爲若干默認大小爲 16KB 的頁。

既然它是一個 pool,那麼必然有滿了的時候,怎麼辦?

就得移除某些頁了,對吧?

那麼問題就來了:移除哪些頁呢?

剛剛說了,它是爲了減小磁盤 IO。因此應該淘汰掉好久沒有被訪問過的頁。

好久沒有使用,這不就是 LRU 的主場嗎?

可是在 MySQL 裏面並非簡單的使用了 LRU 算法。

由於 MySQL 裏面有一個預讀功能。預讀的出發點是好的,可是有可能預讀到並不須要被使用的頁。

這些頁也被放到了鏈表的頭部,容量不夠,致使尾部元素被淘汰。

哦豁,下降命中率了,涼涼。

還有一個場景是全表掃描的 sql,有可能直接把整個緩衝池裏面的緩衝頁都換了一遍,影響其餘查詢語句在緩衝池的命中率。

那麼怎麼處理這種場景呢?

把 LRU 鏈表分爲兩截,一截裏面放的是熱數據,一截裏面放的是冷數據。

打住,不能再說了。

再說就是另一篇文章了,點到爲止。

若是你不清楚,建議去學習一下哦。

LRU 在 Redis 中的應用

既然是內存淘汰算法,那麼咱們經常使用的 Redis 裏面必然也有對應的實現。

Redis 的內存淘汰策略有以下幾種:

  • noenviction:默認策略。不繼續執行寫請求(DEL 請求能夠處理),讀請求能夠繼續進行。這樣能夠保證不會丟失數據,可是會讓線上的業務不能持續進行。
  • volatile-lru:從已設置過時時間的數據集中挑選最近最少使用的數據淘汰。沒有設置過時時間的 key 不會被淘汰。
  • volatile-random:從已設置過時時間的數據集中隨機選擇數據淘汰。
  • volatile-ttl:從已設置過時時間的數據集中挑選將要過時的數據淘汰。
  • allkeys-lru:和 volatile-lru 不一樣的是,這個策略要淘汰的 key 對象是全體的 key 集合。
  • allkeys-random:從全部數據集中隨機選擇數據淘汰。

Redis 4.0 以後,還增長了兩個淘汰策略。

  • volatile-lfu:對有過時時間的 key 採用 LFU 淘汰算法
  • allkeys-lfu:對所有 key 採用 LFU 淘汰算法

關於 Redis 中的 LRU 算法,官網上是這樣說的:

https://github.com/redis/redis-doc/blob/master/topics/lru-cache.md

在 Redis 中的 LRU 算法不是嚴格的 LRU 算法。

Redis 會嘗試執行一個近似的LRU算法,經過採樣一小部分鍵,而後在採樣鍵中回收最適合的那個,也就是最久沒有被訪問的那個(with the oldest access time)。

然而,從 Redis3.0 開始,改善了算法的性能,使得更接近於真實的 LRU 算法。作法就是維護了一個回收候選鍵池。

Redis 的 LRU 算法有一個很是重要的點就是你能夠經過修改下面這個參數的配置,本身調整算法的精度。

maxmemory-samples 5

最重要的一句話我也已經標誌出來了:

The reason why Redis does not use a true LRU implementation is because it costs more memory.

Redis 沒有使用真實的 LRU 算法的緣由是由於這會消耗更多的內存。

而後官網上給了一個隨機 LRU 算法和嚴格 LRU 算法的對比圖:

對於這個圖官網是這樣說的:

你能夠從圖中看到三種不一樣的小圓點造成的三個不一樣的帶:

  • 淺灰色帶是被回收(被 LRU 算法淘汰)的對象
  • 灰色帶是沒有被回收的對象
  • 綠色帶是新添加的對象

因爲 Redis 3.0 對 LRU 算法進行了改進,增長了淘汰池。

因此你能夠看到,一樣使用 5 個採樣點,Redis 3.0 表現得比 Redis 2.8 要好。

同時能夠看出,在 Redis 3.0 中使用 10 爲採樣大小,近似值已經很是接近理論性能。

寫到這裏我忽然想起了另一個面試題。

數據庫中有 3000w 的數據,而 Redis 中只有 100w 數據,如何保證 Redis 中存放的都是熱點數據?

這個題你說它的考點是什麼?

考的就是淘汰策略呀,同志們,只是方式比較隱晦而已。

咱們先指定淘汰策略爲 allkeys-lru 或者 volatile-lru,而後再計算一下 100w 數據大概佔用多少內存,根據算出來的內存,限定 Redis 佔用的內存。

搞定。

才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在後臺提出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。

還有,歡迎關注我呀。

相關文章
相關標籤/搜索