1. 背景
在擼程式碼時,利用區域性性原理對資料做快取是一種常用的效能優化手段。
要做快取,離不開的就是快取元件。ccache就是一個很優秀的lru快取元件,其做了很多很巧妙的優化策略來降低鎖衝突,實現高效能。
降低鎖衝突的策略有
- 一個元素在累計被訪問多次後才做提權(提權指將元素移動到lru鏈的頭部)
- 將提權操作放到一個佇列中,由一個單獨的執行緒做處理
- 在同一個執行緒中做垃圾回收操作
下面看下具體是怎麼實現的。
2. lru cache
在分析原始碼前,先簡單瞭解下lru cache是做什麼的。
lru為least recently used的縮寫,顧名思義,lru cache在快取滿後,再快取新內容需先淘汰最久未訪問的內容。
要實現lru策略,一般是用hashtable和list資料結構來實現,hashtable支援通過key快速檢索到對應的value,list用來記錄元素的訪問時間序,支援淘汰最久未訪問的內容。如下圖
在hashtable中,key對應的內容包含兩部分,第一部分為實際要儲存的內容,這裡定義為value,第二部分是一個指標,指向對應在list中的節點,將其定義為element。
在list中,每個節點也包含兩個部分,第一個部分是一個指標,指向hashtable中對應的value,這裡定義為node,第二部分是next指標,用來串起來整個連結串列。
若我們執行get(key2)操作,會先通過key2找到value2和element2,通過element2又能找到node2,然後將node2移動到list隊首,所以執行完get(key2)後,上圖會變為
這時假如又有一個set(key5, value5)操作,而我們的cache最多隻能快取4條資料,會怎麼處理呢。首先會在hashtable中插入key5和value5,並且在list的隊首插入node5,然後取出list隊尾的元素,這裡是node4,將其刪除,同時刪除node4對應的在hashtable中的資料。執行完上圖會變成
通過上述流程,可以很好的實現lru策略。但是因hashtable和list這兩種資料結構都不是執行緒安全的,若要在多執行緒環境下使用,無論set操作還是get操作都需要加鎖,這樣就會很影響效能,特別是現在的伺服器cpu核心數量越來越多,加鎖對效能的損耗是非常大的。
3. ccache優化策略
針對上面的問題,ccache採用了下面幾種優化策略,都非常的巧妙。
3.1 對hashtable做分片
這是個很常見的策略。
將一個hashtable根據key拆分成多個hashtable,每個hashtable對應一個鎖,鎖粒度更細,衝突的概率也就更低了。
如圖所示,一個hashtable根據key拆分成三個hashtable,鎖也變成了三個。這樣當併發訪問hashtable1和hashtable2時,就不會衝突了。
3.2 累計訪問多次才做提權
value中新增一個訪問計數,每次get操作時,計數+1。當計數達到閾值時,才將其移動到list的隊首,同時將計數重置為0。
如閾值是3,那麼對list的寫操作就會降低3倍,鎖衝突的概率也會減少3倍。
這是一個有損的策略,會使list的順序不完全等同於訪問時間序。但考慮到lru cache的get操作頻率很高,這種策略對命中率的損失應該是可以忽略的。
3.3 單開一個執行緒更新list
在get和set操作時,都需要更新記錄訪問時間序的list,但更新操作只需要在下次set操作前完成就可以,並不需要實時更新。基於這一點,可以單獨開一個更新執行緒對list做更新。get和set時,提交更新任務到佇列中,更新執行緒不停從佇列中取任務做更新。
這樣做有兩個好處
- list不存在多執行緒訪問,不需加鎖
- 操作完hashtable直接返回,非同步更新list,函式相應速度更快
這樣會帶來一個問題,當cpu核心很多,get和set的qps很高時,這個更新執行緒可能成為瓶頸。不過考慮到list的操作是非常輕量的,再加上服務不可能全部資源都放到讀寫cache上,這點也是可以忽略的。
3.4 批量淘汰
當快取滿了後,一次淘汰一批元素。優化在快取滿了的時候,每次set新元素都會觸發淘汰的問題。
3.5 整體流程
在實現完上述策略後,整體流程大致是這樣的