緩存那些事,一是內存爆了要用LRU(最近最少使用)、LFU(最少訪問次數)、FIFO的算法清理一些;二是設置了超時時間的鍵過時便要刪除,用主動或惰性的方法。html
在看全部的細節以前,能夠看一篇至關專業的《緩存算法》,世界真寬闊,算法真奇妙。java
今天看Redis3.0的發行通告裏說,LRU算法大幅提高了,就翻開源碼來八卦一下,結果啼笑皆非,這舊版的"近似LRU"算法,實在太簡單,太偷懶,太Redis了。git
在Github的Redis項目裏搜索lru,找到代碼在redis.c的freeMemoryIfNeeded()函數裏。github
先看2.6版的代碼: 居然就是隨機找三條記錄出來,比較哪條空閒時間最長就刪哪條,而後再隨機三條出來,一直刪到內存足夠放下新記錄爲止.......可憐我看配置文檔後的想象,一直覺得它會幫我在整個Redis裏找空閒時間最長的,哪想到我有一百萬條記錄的時候,它隨便找三條就開始刪了。redis
好,收拾心情再看3.0版的改進:如今每次隨機五條記錄出來,插入到一個長度爲十六的按空閒時間排序的隊列裏,而後把排頭的那條刪掉,而後再找五條出來,繼續嘗試插入隊列.........嗯,好了一點點吧,起碼每次隨機多了兩條,起碼不僅在一次隨機的五條裏面找最久那條,會連同以前的一塊兒作比較......算法
相比之下,Memcached實現的是再標準不過的LRU算法,專門使用了一個教科書式的雙向鏈表來存儲slab內的LRU關係,代碼在item.c裏,詳見memcached源碼分析-----LRU隊列與item結構體,元素插入時把本身放到列頭,刪除時把本身的先後兩個元素對接起來,更新時先作刪除再作插入。c#
分配內存超限時,很天然就會從LRU的隊尾開始清理。api
Guava Cache一樣作了一個雙向的Queue,見LocalCache中的AccessQueue類,也會在超限時從Queue的隊尾清理,見evictEntries()函數。緩存
看文檔,竟然和Redis2.6同樣,直接隨機8條記錄,找出最舊那條,刷到磁盤裏,再看代碼,Eviction類和 OnHeapStore的evict()函數。oracle
再看Hazelcast,幾乎同樣,隨機取25條。 這種算法,切換到LFU也很是簡單。
不事後來再想一想,也許Redis原本就不是主打作Cache的,這種內存爆了須要經過LRU刪掉一些元素不是它的主要功能,默認設置都是noeviction——內存不夠直接報錯的,因此就懶得建個雙向鏈表,並且每次訪問時都要更新它了,看Google Group里長長的討論,新版算法也是社區智慧的結晶。況且,Ehcache和Hazelcast也是和它的舊版同樣的算法,Redis的新版還比這二者強了。
後來,根據@劉少壯同窗的提示,JBoss的InfiniSpan裏還實現了比LRU更高級的LIRS算法,能夠避免一些冷數據由於某個緣由被大量訪問後,把熱數據擠佔掉。
若是能爲每個設置了過時的元素啓動一個Timer,一到時間就觸發把它刪掉,那無疑是能最快刪除過時鍵最省空間的,在Java裏用一條DeplayQueue存着,開條線程不斷的讀取就能作到。但由於該線程消耗CPU較多,在內存不緊張時有點浪費,彷佛你們都不用這個方法。
因此有了惰性檢查,就是每次元素被訪問時,纔去檢查它是否已經超時了,這個各家都同樣。但若是那個元素後來都沒再被訪問呢,會永遠佔着位子嗎?因此各家都再提供了一個按期主動刪除的方式。
代碼在redis.c的activeExpireCycle()裏,看過文檔的人都知道,它會在主線程裏,每100毫秒執行一次,每次隨機抽20條Key檢查,若是有1/4的鍵過時了,證實此時過時的鍵可能比較多,就不等100毫秒,馬上開始下一輪的檢查。不過爲免把CPU時間都佔了,又限定每輪的總執行時間不超過1毫秒。
Memcached裏有個文不對題的LRU爬蟲線程,利用了以前那條LRU的隊列,能夠設置多久跑一次(默認也是100毫秒),沿着列尾一直檢查過去,每次檢查LRU隊列中的N條數據。雖然每條Key設置的過時時間可能不同,但怎麼說命中率也比Redis的隨機選擇N條數據好一點,但它沒有Redis那種過時的多了立馬展開下一輪檢查的功能,因此每秒最多隻能檢查10N條數據,須要本身本身權衡N的設置。
在Guava Cache裏,同一個Cache裏全部元素的過時時間是同樣的,因此它比Memached更方便,順着以前那條LRU的Queue檢查超時,不限定個數,直到不超時爲止。並且它這個檢查的調用時機並非100毫秒什麼的,而是每次各類寫入數據時的preWriteCleanup()方法中都會調用。
吐槽一句,Guava的Localcache類裏面已經4872行了,一點都不輕量了。
Ehcache更亂,首先它的內存存儲中只有惰性檢查,沒有主動檢查過時的,只會在內存超限時不斷用近似LRU算法(見上)把內存中的元素刷到磁盤中,在文件存儲中才有超時檢查的線程,FAQ裏專門解釋了緣由。
而後磁盤存儲那有一條8小時左右跑一次的線程,每次遍歷全部元素.....見DiskStorageFactory裏的DiskExpiryTask。 一圈看下來,Ehcache的實現最弱。