作為一名後端軟體工程師,工作中你肯定和 Redis 打過交道。但是Redis 為什麼快呢?很多人只能答出Redis 因為它是基於記憶體實現的,但是對於其它原因都是模稜兩可。
那麼今天就一起來看看是Redis 為什麼快吧:
Redis 是基於記憶體的資料庫,那不可避免的就要與磁碟資料庫做對比。對於磁碟資料庫來說,是需要將資料讀取到記憶體裡的,這個過程會受到磁碟 I/O 的限制。而對於記憶體資料庫來說,本身資料就存在於記憶體裡,也就沒有了這方面的開銷。
通過下面的表格我們可以知道讀取記憶體和讀取磁碟的效能差距。
計算機裝置 |
讀取的速度 |
類比 |
機械硬碟 |
0.1G/S |
以機械盤為基準 |
固態盤 |
1.3G/S |
13倍機械硬碟 |
記憶體 |
30G/S |
300倍機械硬碟 |
L3 |
190G/S |
1900倍機械硬碟 |
L2 |
200G/S |
2000倍 機械硬碟 |
L1 |
800G/S |
8000倍機械硬碟 |
為了實現從鍵到值的快速存取,Redis 使用了一個雜湊表來儲存所有鍵值對。一個雜湊表,其實就是一個陣列,陣列的每個元素稱為一個雜湊桶。所以,我們常說,一個雜湊表是由多個雜湊桶組成的,每個雜湊桶中儲存了鍵值對資料。
雜湊桶中的 entry 元素中儲存了key和value指標,分別指向了實際的鍵和值,因為其value的多樣性,雜湊表中儲存的並不是具體的值,而是一個記憶體參照地址,在通過記憶體參照的地址查詢到對應的具體的值。這樣一來,即使value是一個集合,也可以通過*value指標被查詢到。因為這個雜湊表儲存了所有的鍵值對,所以,我也把它稱為全域性雜湊表。
雜湊表的最大好處很明顯,就是讓我們可以用 O(1) 的時間複雜度來快速查詢到鍵值對:我們只需要計算鍵的雜湊值,就可以知道它所對應的雜湊桶位置,然後就可以存取相應的 entry 元素。但當你往 Redis 中寫入大量資料後,就可能發現操作有時候會突然變慢了。這其實是因為你忽略了一個潛在的風險點,那就是雜湊表的衝突問題和 rehash 可能帶來的操作阻塞。
當你往雜湊表中寫入更多資料時,雜湊衝突是不可避免的問題。這裡的雜湊衝突,兩個 key 的雜湊值和雜湊桶計算對應關係時,正好落在了同一個雜湊桶中。
Redis 解決雜湊衝突的方式,就是鏈式雜湊。鏈式雜湊也很容易理解,就是指同一個雜湊桶中的多個元素用一個連結串列來儲存,它們之間依次用指標連線。
省去了很多上下文切換的時間以及CPU消耗,不存在競爭條件,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,也不會出現死鎖而導致的效能消耗。
Redis採用了epoll 模型進行IO多路複用。Java中也有類似的模型比如NIO,才epoll模型之前還有selector、poll這裡不多講解,epoll模型可以參考下圖:
Redis是當然如果這個陣列一直不變,那麼hash衝突會變很多,這個時候檢索效率會大打折扣,所以Redis就需要把陣列進行擴容(一般是擴大到原來的兩倍),但是問題來了,擴容後每個hash桶的資料會分散到不同的位置,這裡設計到元素的移動,必定會阻塞IO,所以這個ReHash過程會導致很多請求阻塞。
為了避免這個問題,Redis 採用了漸進式 rehash。
首先、Redis 預設使用了兩個全域性雜湊表:雜湊表 1 和雜湊表 2。一開始,當你剛插入資料時,預設使用雜湊表 1,此時的雜湊表 2 並沒有被分配空間。隨著資料逐步增多,Redis 開始執行 rehash。
1、給雜湊表 2 分配更大的空間,例如是當前雜湊表 1 大小的兩倍
2、把雜湊表 1 中的資料重新對映並拷貝到雜湊表 2 中
3、釋放雜湊表 1 的空間
在上面的第二步涉及大量的資料拷貝,如果一次性把雜湊表 1 中的資料都遷移完,會造成 Redis 執行緒阻塞,無法服務其他請求。此時,Redis 就無法快速存取資料了。
在Redis 開始執行 rehash,Redis仍然正常處理使用者端請求,但是要加入一個額外的處理:
處理第1個請求時,把雜湊表 1中的第1個索引位置上的所有 entries 拷貝到雜湊表 2 中
處理第2個請求時,把雜湊表 1中的第2個索引位置上的所有 entries 拷貝到雜湊表 2 中
如此迴圈,直到把所有的索引位置的資料都拷貝到雜湊表 2 中。
這樣就巧妙地把一次性大量拷貝的開銷,分攤到了多次處理請求的過程中,避免了耗時操作,保證了資料的快速存取。
所以這裡基本上也可以確保根據key找value的操作在O(1)左右。
過這裡要注意,如果Redis中有海量的key值的話,這個Rehash過程會很長很長,雖然採用漸進式Rehash,但在Rehash的過程中還是會導致請求有不小的卡頓。並且像一些統計命令也會非常卡頓:比如keys
按照Redis的設定每個範例能儲存的最大的key的數量為2的32次方,即2.5億,但是儘量把key的數量控制在千萬以下,這樣就可以避免Rehash導致的卡頓問題,如果數量確實比較多,建議採用分割區hash儲存。
我們平常使用系統時間戳時, 常常是不假思索地使用System.currentTimeMillis()或者new Date() .getTime() 來獲取系統的毫秒時間戳。但是Redis不能這樣做,因為每一次獲取系統時間戳都是一次系統呼叫,而且每次去系統呼叫是比較費時間的,作為單執行緒的Redis是無法承受的,所以它需要對於時間戳進行一次快取,由一個定時任務進行每毫秒更新時間戳,從而獲取時間戳都是直接從快取就取出。