我們先從Redis支持的數據類型學起,了解不同的數據類型的差異和底層實現的數據結構。
Redis的數據類型
一般的鍵值對數據庫只支持String一種數據類型,例如Memcached,而Redis支持的數據類型非常豐富,一共有5種,分別是String(字符串)、List(列表)、Hash(字典)、Set(集合)、SortSet(有序集合)。除String外,其余四種數據類型是集合類型。
String
String是最簡單的數據類型了,它是由簡單動態字符串實現的。
List
List支持存儲一組數據,有兩種實現方式:壓縮列表(ziplist)和雙向循環鏈表。
Hash
Hash用來存儲一組數據對,每個數據對又包含鍵和值兩部分。Hash也有兩種實現方式:壓縮列表和哈希表。
Set
Set這種數據類型用來存儲一組不重復的數據。有兩種實現方式:有序數組和哈希表。
SortedSet
SortedSet用來存儲一組數據,並且每個數據會附帶一個得分。通過得分的大小,我們將數據組織成跳表這種數據結構,以支持快速地按照得分值、得分區間獲得數據。
SortedSet也有兩種實現方式:跳表和壓縮列表。
Redis的數據結構
Redis底層一共有六種數據結構,分別是簡單動態字符串、雙向鏈表、壓縮列表、哈希表、跳表、整數數組。
簡單動態字符串(SDS)
與字符串本身不同,簡單動態字符串(SDS)保留字符串長度信息,只需要O(1)就能得到字符串長度。
- len:記錄buf數據中已使用字節的數量
- free:記錄buf數組中未使用字節的數量
- buf:字節數組,用於保存字符串
雙向鏈表
雙向鏈表是節點保存前置和后置節點的指針的鏈表結構。
壓縮列表
類似於數組,但與數組不同的是,壓縮列表在表頭有三個字段(zlbytes、zltail、zllen)和表名有一個字段(zlend)。
查找第一個元素和最后一個元素只需要O(1),其他O(n)。
- zlbytes:列表長度
- zltail:列表層的偏移量
- zllen:列表中的entry個數
- entry list:列表數組
- zlend:表示列表結束
哈希表
一個哈希表,其實就是一個數組,數組的每個元素稱為一個哈希桶。
哈希表最大的好處是查找元素只需要O(1)時間復雜度,但隨着哈希表寫入大量數據后,就可能出現操作變慢的情況,這是因為哈希表的沖突問題和rehash可能帶來的操作阻塞。
為什么哈希表操作變慢了?
當哈希表中寫入大量數據時,哈希沖突是不可避免的。這里的哈希沖突指的是兩個key的哈希值相同,落在同一個哈希桶中。
Redis解決哈希沖突的方式是:鏈式哈希,即同一個哈希桶中的多個元素用一個鏈表來保存。
但這里還存在一個問題,哈希沖突鏈上查找元素的時間復雜度是O(N),當哈希沖突鏈過長,查找元素耗時長,效率降低。這對Redis來說是不能接受的。
所以Redis會對哈希表做rehash操作。rehash就是增加現有的哈希桶數量,即擴容。
Redis執行rehash有三步:
- 給哈希表2分配更大的空間,例如是當前哈希表1大小的兩倍;
- 把哈希表1中的數據重新映射並拷貝到哈希表2中;
- 釋放哈希表1的空間。
這個過程看似簡單,但是步驟2涉及大量的數據拷貝,如果一次性把數據都遷移完,會造成Redis線程阻塞,無法處理其他請求。
為了避免這個問題,Redis采用了漸進式rehash。
簡單來說,就是把步驟2拷貝數據這一次性開銷分攤到多次請求里,例如請求key1,會把key1所在的哈希桶從哈希表1遷移到哈希表2里,直到所有哈希桶都遷移到哈希表2里。在這個過程里,哈希表1要一直保留,等到遷移完成后才釋放。
除了鍵值對請求操作進行數據遷移,Redis本身還會有一個定時任務來執行rehash。
跳表
有序鏈表只能逐一查找元素,導致操作非常緩慢,於是出現了跳表。具體來說,跳表是在鏈表的基礎上,增加了多級索引,通過索引位置的幾個跳轉,實現數據的快速定位。如下圖所示:
可以看到,這個查找過程就是在多級索引上跳來跳去,最后定位到元素。跳表的查找時間復雜度是O(logN)。
整數數組
就是一個數組,元素類型是整數,並且是有序的。
- encoding:編碼方式
- length:數組包含的元素數量
- contents:保存元素的數組
集合類型的時間復雜度
總結
- Redis能夠快速操作鍵值對,一方面是因為使用了O(1)的哈希表,另一方面是也采用了O(logN)的跳表。
- 集合類型的范圍操作的時間復雜度通常是O(N)。建議用其他命令替換,例如SCAN。
- List的復雜度較高,要因地制宜使用List。
拓展
整數數組和壓縮列表在查找時間復雜度方面沒有很大的優勢,Redis為什么還會把它們作為底層數據結構?
兩方面原因:
- 數組和壓縮列表可以提升內存利用率,因為他們的數據結構緊湊
- 數組對CPU高速緩存支持友好,當數據元素超過閾值時,會轉為hash和跳表,保證查詢效率