前言
作為一名后端軟件工程師,工作中你肯定和 Redis 打過交道。但是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消耗,不存在競爭條件,不用去考慮各種鎖的問題,不存在加鎖釋放鎖操作,也不會出現死鎖而導致的性能消耗。
四、使用基于IO多路復用機制的線程模型,可以處理并發的鏈接。
Redis采用了epoll 模型進行IO多路復用。Java中也有類似的模型比如NIO,才epoll模型之前還有selector、poll這里不多講解,epoll模型可以參考下圖:
epoll 模型
五、漸進式ReHash
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 就無法快速訪問數據了。
漸進式rehash
在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是無法承受的,所以它需要對于時間戳進行一次緩存,由一個定時任務進行每毫秒更新時間戳,從而獲取時間戳都是直接從緩存就取出。
審核編輯:黃飛
?
評論