go語言高性能緩存組件ccache分析

1. 背景

在擼代碼時,利用局部性原理對數據作緩存是一種經常使用的性能優化手段。node

要作緩存,離不開的就是緩存組件。ccache就是一個很優秀的lru緩存組件,其作了不少很巧妙的優化策略來下降鎖衝突,實現高性能。git

下降鎖衝突的策略有github

  • 一個元素在累計被訪問屢次後才作提權(提權指將元素移動到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 總體流程

在實現完上述策略後,總體流程大體是這樣的

3.5.1 get操做

3.5.2 set操做

相關文章
相關標籤/搜索