《Redis設計與實現》第一部分

前言

工作中經常會用到Redis,雖說會一些Redis的增刪改查的API基本可以滿足日常工作的需求,但是在一些時候還是會遇到一些疑惑。比如在插入string類型然後取出來的時候我遇到過解碼問題,還有插入二進制數據的一些問題。當然,面試的時候Redis也是經常會被問到的一個問題。聽說Redis的實現是非常精妙的,代碼質量也非常高。總之是時候瞭解一下Redis的底層實現了。

第一部分 數據結構與對象

簡單動態字符串

Redis不是使用的C語言的傳統字符串而是構建了一種簡單動態字符串SDS的抽象類型。

SDS用來保存數據庫中的字符串值,也被用作緩衝區(buffer)。在這裏插入圖片描述

  • 記錄長度的另外一個好處就是可以杜絕緩衝區溢出
  • sds的另外一個屬性free記錄的是還有多少空餘的空間,這樣可以實現空間預分配和惰性空間釋放兩種優化策略。
  • SDS是二進制安全的,因爲不以\0作爲結束,而是以長度做判斷,所以SDS可以存儲任意二進制信息。
  • SDS也可以複用一些C語言的庫函數。

鏈表

當一個列表鍵包含了很多元素,或者列表中包含的元素都是比較長的字符串時,Redis就會使用鏈表作爲列表鍵的底層實現。
在這裏插入圖片描述
在這裏插入圖片描述

  • 雙端,無環,帶頭尾指針,長度計數器
  • 多態:通過爲鏈接設置不同的類型特定函數,Redis的鏈表可以用於保存不同類型的值。

字典

Redis的數據庫就是使用字典來作爲底層實現,增刪改查也是構建在對字典的操作之上。
字典使用哈希表作爲底層實現。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
看到這裏發現Redis的字典的實現其實和Java裏面字典的實現非常像,都是一個數組,通過hash值作爲索引,然後用鏈表來解決鍵的hash衝突。
在這裏插入圖片描述
關於rehash,我理解是用來擴展大小用的。
https://www.jianshu.com/p/13c650a25ed3

  1. 首先創建一個比現有哈希表更大的新哈希表(expand)
  2. 然後將舊哈希表的所有元素都遷移到新哈希表去(rehash)
    下面這張圖就很好的說明了Redis的字典結構了:
    在這裏插入圖片描述

hash算法

添加鍵值對時,通過鍵計算出hash值和索引。
在這裏插入圖片描述
解決鍵衝突的方式也適合hashmap一樣用一個鏈表,新加的鍵放在表頭位置,保證O(1)

rehash

看到這裏明白dict.ht[2]了,有兩個ht是爲了rehash時使用的,擴展或縮小時,吧ht[0]的rehash一下,放到ht[1],然後釋放0,把1再放到0上。這就完成了dict的縮放。
這一rehash過程也不是一次完成,如果多的話需要漸進式rehash,用dict.trehashidx來表示是否在rehash,如果是-1表示不在rehash。

跳躍表

在每個節點中維持多個指向其他節點的指針,達到快速訪問節點的目的。Redis使用跳躍表來作爲有序集合鍵的底層實現。
跳錶的效率和紅黑樹以及 AVL 樹不相上下,但跳錶的原理相當簡單,只要你能熟練操作鏈表,就能輕鬆實現一個 SkipList。
關於跳錶的概念下面兩個博客寫得比較好:
https://www.cnblogs.com/thrillerz/p/4505550.html
https://www.cnblogs.com/Leo_wl/p/11557614.html

有序表

考慮一個有序表:
在這裏插入圖片描述
從該有序表中搜索元素 < 23, 43, 59 > ,需要比較的次數分別爲 < 2, 4, 6 >,總共比較的次數
爲 2 + 4 + 6 = 12 次。有沒有優化的算法嗎? 鏈表是有序的,但不能使用二分查找。類似二叉
搜索樹,我們把一些節點提取出來,作爲索引。得到如下結構:
在這裏插入圖片描述
這裏我們把 < 14, 34, 50, 72 > 提取出來作爲一級索引,這樣搜索的時候就可以減少比較次數了。
我們還可以再從一級索引提取一些元素出來,作爲二級索引,變成如下結構:
在這裏插入圖片描述
這裏元素不多,體現不出優勢,如果元素足夠多,這種索引結構就能體現出優勢來了。

跳躍表

其中 -1 表示 INT_MIN, 鏈表的最小值,1 表示 INT_MAX,鏈表的最大值。
在這裏插入圖片描述
跳錶具有如下性質:
(1) 由很多層結構組成
(2) 每一層都是一個有序的鏈表
(3) 最底層(Level 1)的鏈表包含所有元素
(4) 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現。
(5) 每個節點包含兩個指針,一個指向同一鏈表中的下一個元素,一個指向下面一層的元素。

跳錶的搜索

在這裏插入圖片描述
例子:查找元素 117
(1) 比較 21, 比 21 大,往後面找
(2) 比較 37, 比 37大,比鏈表最大值小,從 37 的下面一層開始找
(3) 比較 71, 比 71 大,比鏈表最大值小,從 71 的下面一層開始找
(4) 比較 85, 比 85 大,從後面找
(5) 比較 117, 等於 117, 找到了節點。

跳錶的插入

先確定該元素要佔據的層數 K(採用丟硬幣的方式,這完全是隨機的)
然後在 Level 1 … Level K 各個層的鏈表都插入元素。
例子:插入 119, K = 2
在這裏插入圖片描述
如果 K 大於鏈表的層數,則要添加新的層。
例子:插入 119, K = 4
在這裏插入圖片描述丟硬幣決定 K
插入元素的時候,元素所佔有的層數完全是隨機的
相當與做一次丟硬幣的實驗,如果遇到正面,繼續丟,遇到反面,則停止,
用實驗中丟硬幣的次數 K 作爲元素佔有的層數。顯然隨機變量 K 滿足參數爲 p = 1/2 的幾何分佈,
K 的期望值 E[K] = 1/p = 2. 就是說,各個元素的層數,期望值是 2 層。

跳錶的刪除

在各個層中找到包含 x 的節點,使用標準的 delete from list 方法刪除該節點。

例子:刪除 71
在這裏插入圖片描述

跳躍表節點實現:
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

整數集合

當一個集合只包含整數值元素,而且數量不多時,Redis就會使用整數集合作爲集合鍵的底層實現。
在這裏插入圖片描述

集合升級降級

如果之前都是int16,然後要存一個int32的數字,就要對整數集合進行升級,重新分配空間,然後把現有的元素轉型。
整數集合不支持降級操作。

壓縮列表

是列表鍵和hash鍵的底層實現之一
壓縮列表結構
每個壓縮列表的節點(entry)都可以保存一個字節數組或者一個整數值。
entry的構成:在這裏插入圖片描述
這張圖片其實比較好的說明了壓縮列表。
這篇博客的內容關於Redis壓縮列表的介紹更詳細:https://www.cnblogs.com/hunternet/p/11306690.html 在這裏插入圖片描述

對象

前面介紹的幾種數據結構,Redis並沒有直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構創建了一個對象系統。
一個優點就是可以針對不同的使用場景來爲對象設置多種不同的數據結構實現,從而優化對象在不同場景下的使用效率。
Redis還實現了基於引用計數的內存回收機制,程序不再使用某個對象的時候,這個對象佔用的內存就會被釋放。還有空轉市場這個概念。
對象結構:
在這裏插入圖片描述
type屬性有字符串,列表,hash,集合,有序集合。
在這裏插入圖片描述
ptr指向的是對象的底層實現數據結構。由encoding屬性決定。
在這裏插入圖片描述

字符串對象

字符串對象的編碼可以是int,rwa,embstr
例子:在這裏插入圖片描述

列表對象

底層可以使ziplist或者linkedlist。
使用壓縮表的列表對象ptr指向一個ziplist。
雙端鏈表對象每個節點都是一個字符串對象。後面的hash,集合等對象也會嵌套字符串對象。
在這裏插入圖片描述

哈希對象

編碼可以使壓縮列表或者hash表。在這裏插入圖片描述
在這裏插入圖片描述

集合對象

intset或者hashtable

有序集合

ziplist or skiplist

類型檢查

類型檢查這個東西其實就是多態,針對不同的類型有不同的處理方式。

內存回收

基於引用計數實現的內存回收機制

對象共享

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章