1. 程式人生 > >常用快取技術

常用快取技術

這是使用快取最頻繁最直接的方式,即我們把需要頻繁訪問DB的資料載入到記憶體裡面,以提高響應速度。通常我們的做法是使用一個ConcuccrentHashMap<Request, AtomicInteger>來記錄一天當中每個請求的次數,每天凌晨取出昨天訪問最頻繁的K個請求(K取多少個取決你的可用記憶體有多少),從DB中讀取這些請求的返回結果放到一個ConcuccrentHashMap<Request, Response>容器中,然後把所有請求計數清0,重新開始計數。

LRU快取

熱資料快取適用於那些熱資料比較明顯且穩定的業務場景,而對於那些熱資料不穩定的應用場景我們需要發明一種動態的熱資料識別方式。我們都知道常用的記憶體換頁演算法有2種:LFU和LRU。

LFU(Least Frequently Used)是把那些最近最不經常使用的頁面置換出去,這跟上面講的熱資料快取是一個道理,缺點有2個:

  1. 需要維護一個計數器,記住每個頁面的使用次數。
  2. 上一個時間段頻繁使用的,在下一個時間段不一定還頻繁。

LRU(Least Recently Used)策略是把最近最長時間未使用的頁面置換出去。實現起來很簡單,只需要一個連結串列結構,每次訪問一個元素時把它移到連結串列的尾部,當連結串列已滿需要刪除元素時就刪除頭部的元素,因為頭部的元素就是最近最長時間未使用的元素。

View Code

TimeOut快取

Timeout快取常用於那些跟使用者關聯的請求資料,比如使用者在翻頁檢視一個列表資料時,他第一次看N頁的資料時,伺服器是從DB中讀取的相應資料,當他看第N+1頁的資料時應該把第N頁的資料放入快取,因為使用者可能呆會兒還會回過頭來看第N頁的資料,這時候伺服器就可以直接從快取中獲取資料。如果使用者在5分鐘內還沒有回過頭來看第N頁的資料,那麼我們認為他再看第N頁的概率就非常低了,此時可以把第N頁的資料從快取中移除,實際上相當於我們為快取設定了一個超時時間。

我想了一種Timeout快取的實現方法。還是用ConcurrentHashMap來存放key-value,另建一棵小頂堆,每個節點上存放key以及key的到期時間,建堆時依據到期時間來建。開一個後臺執行緒不停地掃描堆頂元素,拿當前的時間戳去跟堆頂的到期時間比較,如果當前時間晚於堆頂的到期時間則刪除堆頂,把堆頂裡存放的key從ConcurrentHashMap中刪除。刪除堆頂的時間複雜度為O(log2N)O(log2N),具體步驟如下:

  1. 用末元素替換堆頂元素root

  2. 臨時儲存root節點。從上往下遍歷樹,用子節點中較小那個替換父節點。最後把root放到葉節點上

下面的程式碼是直接基於java中的java.util.concurrent.Delayed實現的,Delayed是不是基於上面的小頂堆的思想我也沒去深入研究。

TimeoutCache.java

View Code

DelayItem.java

View Code

JavaSerializer.java

View Code

Redis省記憶體的技巧

 redis自帶持久化功能,當它決定要把哪些資料換出記憶體寫入磁碟時,使用的也是LRU演算法。同時redis也有timeout機制,但它不像上面的TimeoutCache.java類一樣開個無限迴圈的執行緒去掃描到期的元素,而是每次get元素時判斷一個該元素有沒有到期,所以redis中一個元素的存活時間遠遠超出了設定的時間是很正常的。

本節想講的重點其實是redis省記憶體的技巧,這也是實踐中經常遇到的問題,因為記憶體總是很昂貴的,運維大哥總是很節約的。在我們的推薦係數中使用Redis來儲存資訊的索引,沒有使用Lucene是因為Lucene不支援分散式,但是省記憶體的技巧都是從Lucene那兒學來的。

首先,如果你想為redis節省記憶體那你就不能再用<String,String>型別的key-value結構,必須全部將它們序列化成二進位制的形式。我寫了一個工具類,實現各種資料型別和byte[]的互相置換。

DataTransform.java

View Code

請留意一下上述程式碼中出現了VInt和VLong兩種型別,具體看註釋。

倒排索引常見的形式為:term -->  [infoid1,infoid2,infoid3...],針對這種形式的索引我們看下如何節省記憶體。首先value要採用redis中的list結構,而且是list<byte[]>而非list<String>(想省記憶體就要杜絕使用String,上面已經說過了)。假如infoid是個int,置換成byte[]就要佔4個位元組,而絕大部分情況下infoid都1000萬以內的數字,因此使用VInt只需要3個位元組。記憶體還可以進一步壓縮。連結串列的第1個infoid我們儲存它的VInt形式,後面的infoid與infoid1相減,差值也是個1000萬以內的數字而且有可能非常小,我們採用VInt儲存這個差值最多需要3個位元組,有可能只需要1個位元組。訪問連結串列中的任意一個元素時都需要先把首元素取出來。

另一種常見的索引形式為:infoid --> infoDetail,infoDetail中包含很多欄位,譬如city、valid、name等,通常情況下人們會使用Redis的hash結構來儲存實體,而我們現在要做的就是把infoDetail這個實體序列化成儘可能短的位元組流。首先city代表城市,本來是個String型別,而city這個東西是可以窮舉的,我們事先對所有city進行編號,在redis中只儲存city編號即可。valid表示資訊是否過期是個bool型別,在java中儲存一個bool也需要1個位元組,這顯然很浪費,本來一個bit就夠了嘛,同時city又用不滿一個int,所以可以讓valid跟city擠一擠,把city左移一位,把valid塞到city的末位上去。

View Code

 

原文來自:部落格園(華夏35度)http://www.cnblogs.com/zhangchaoyang 作者:Orisun