Design a high performance cache for multi-threaded environment

如何設計一個支持高併發的高性能緩存庫html

不 考慮併發狀況下的緩存的設計你們應該都比較清楚,基本上就是用map/hashmap存儲鍵值,而後用雙向鏈表記錄一個LRU來用於緩存的清理。這篇文章 應該是講得很清楚http://timday.bitbucket.org/lru.html。可是考慮到高併發的狀況,如何才能讓緩存保持高性能呢?redis

高併發緩存須要解決2個問題:1. 高效率的內存分配;2. 高效率的讀取,插入和清理數據。關於第一個問題涉及到高效率的內存分配器,使用成熟的jemalloc/tcmalloc足夠知足需求。這裏探討下如何解決第二個問題。數組

由 於緩存系統的特色, 每次讀取緩存都須要更新一些訪問信息(最後讀取時間,訪問頻率),在清理時會根據這些信息使用不一樣的策略來進行數據清理,這樣看來彷佛每次的讀操做都變成 了寫操做。看過幾篇文章都比較集中在如何優化這個讀操做修改LRU的行爲。例如: http://www.ebaytechblog.com/2011 /08/30/high-throughput-thread-safe-lru-caching/#.UzvEb3V53No 以及 http://openmymind.net/High-Concurrency-LRU-Caching/, 可是這種狀況下不論怎麼優化,使用鏈表的LRU始終是個瓶頸, 由於每次讀操做只能一個線程來修改這個LRU鏈表,而且修改都是集中在鏈表的兩端。有些文章甚至使用lock-free的doubled linked list來減小鎖競爭。 一些成熟的緩存系統如memcached,使用的是全局的LRU鏈表鎖,而redis是單線程的因此不須要考慮併發的問題。因爲這些都是遠程的緩存服務 器,所以它們的瓶頸每每是網卡,因此併發上面並不須要什麼高要求。緩存

仔 細思考後,發如今併發的狀況下使用LRU鏈表來記錄訪問信息其實並不合適,會致使嚴重的鎖競爭,這點沒法避免。所以,須要完全放棄使用LRU鏈表。鑑於緩 存系統的特性,能夠作以下假設: 緩存若是達到閥值,可使插入當即返回失敗。這樣,咱們可使用一個預分配的數組來記錄全部的cache item的訪問信息。整個結構以下:安全

concurrent cache system arch網絡

可 以看到鎖粒度是hashmap裏面的bucket級別,每次讀操做只需對相應的bucket加鎖,而後更新bucket對應的訪問信息數組元素便可,因爲 每一個bucket對應的訪問信息是獨立佔據數組的一個元素的,所以bucket鎖就保證了訪問信息的線程安全。這樣就解決了讀取操做的併發競爭問題。多線程

接 下來看看如何解決插入問題。從圖能夠看到,每一個bucket的須要分配一個獨立的access info位置索引,多線程插入的時候會發生競爭,爲了減小競爭,能夠預先生成一個目前空閒的位置鏈表,這樣插入的時候,每一個線程能夠根據當前的 bucket索引選擇從不一樣的free鏈表裏面分配一個位置。這樣鎖競爭能夠分散到多個free list上面,每次插入時把分配過的位置索引從free list 移除。併發

最 後,清理過程能夠放在一個獨立線程裏面,爲了不插入由於緩存滿了而返回失敗,每次在緩存快滿的時候(free list的size不夠用了),進行一次access info array掃描。根據不一樣的緩存清除策略和訪問信息(時間和頻率)來決定哪些位置索引是能夠從新釋放到free list列表。因爲掃描過程當中無需加鎖,掃描對讀取和插入操做是沒有性能影響的。只有最後進行釋放時纔會對須要釋放的bucket和free list進行加鎖,鎖競爭大大減小。dom

如上設計,大大減小了緩存的讀取,插入和清理過程當中的鎖競爭問題,而且讀取和插入都是O(1)的,並不會由於緩存系統的增大影響性能(清理後臺線程可能會跑的久點,能夠選擇性清理來優化)。這樣一個支持高併發的緩存系統就完成了。socket

簡 單的實現後,實測在8核CPU上面8線程讀,8線程寫,能夠跑到 讀寫TPS均在1M/S以上,參考官方單線程的redis的benchmark數據 Using a unix domain socket 排除網絡瓶頸,SET/GET的TPS大概在200k/s 左右,能夠看到這樣一個高併發的cache基本上是scalable的。

相關文章
相關標籤/搜索