1. 前言
簡(jiǎn)單來(lái)說(shuō),Redis(Remote Dictionary Server)也是一個(gè)數(shù)據(jù)庫(kù),不過(guò)和傳統(tǒng)數(shù)據(jù)庫(kù)不同點(diǎn)在于 Redis 的數(shù)據(jù)存儲(chǔ)在內(nèi)存中,所以讀寫速度遠(yuǎn)超傳統(tǒng)數(shù)據(jù)庫(kù)(例如 MySQL ),同時(shí)因?yàn)?code>Key—Value的數(shù)據(jù)存儲(chǔ)形式非常零活,所以Redis被廣泛應(yīng)用在緩存方向,并且能適配各種實(shí)戰(zhàn)的應(yīng)用場(chǎng)景。
Redis 提供了多種語(yǔ)言的 API ,比如常見(jiàn)的 Java、C++、Python 等,基本是后端開發(fā)中最常用的緩存中間件。
掌握 MySQL 是后端開發(fā)的必備技能,掌握 Redis 則是一個(gè)簡(jiǎn)歷加分項(xiàng)。
我們?cè)谥暗恼鹿?jié)對(duì) MySQL 常見(jiàn)的題目進(jìn)行了剖析, 在本章中我們會(huì)介紹 Redis 相關(guān)的高頻面試題。
2. Redis底層數(shù)據(jù)結(jié)構(gòu)
面試官提問(wèn): 你有看過(guò) Redis 源碼嗎?Redis 底層是用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的?
題目解析:
這里談到的數(shù)據(jù)結(jié)構(gòu)不是 Redis 的五種對(duì)外基本數(shù)據(jù)結(jié)構(gòu):String(字符串類型)、Hash(哈希類型)、List(鏈表類型)、Set(集合類型)、ZSet(有序集合類型),而是更為底層的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),例如雙向鏈表、字典、壓縮列表等。
Redis 底層是用標(biāo)準(zhǔn) C 語(yǔ)言編寫的,下面我們會(huì)結(jié)合 C 代碼分析。
2.1 SDS 數(shù)據(jù)結(jié)構(gòu)
2.1.1 定義
字符串是 Redis 中最常用的數(shù)據(jù)類型。
Redis 里的五種基礎(chǔ)數(shù)據(jù)類型都是以唯一字符串作為 Key,通過(guò)這個(gè) Key 映射不同的 Value 數(shù)據(jù),不同數(shù)據(jù)類型的差異在于存儲(chǔ) Value 不同,String 類型的 Value 是字符串,List 類型的 Value 是列表。
Redis 沒(méi)有使用 C 語(yǔ)言原生的字符數(shù)組表示字符串,而是定義了一個(gè)字符串結(jié)構(gòu)體 SDS(Simple Dynamic String,簡(jiǎn)單動(dòng)態(tài)字符串)。
SDS的結(jié)構(gòu)體定義如下:
struct sdshdr{
//記錄buf數(shù)組中已使用字節(jié)的數(shù)量,等于SDS中已使用字符串的長(zhǎng)度
int len;
//記錄buf數(shù)組中未使用字節(jié)的數(shù)量
int free;
//字節(jié)數(shù)組,用于保存字符串
char buf[];
}
然后候選人可以結(jié)合畫圖的方式解釋SDS的數(shù)據(jù)結(jié)構(gòu):
?
如上圖所示,當(dāng)前字符串長(zhǎng)度len = 4,未使用長(zhǎng)度f(wàn)ree = 0,buf數(shù)組存儲(chǔ)的內(nèi)容是"mooc\0"
。
注意:C 語(yǔ)言中的字符串都是以'\0'
結(jié)尾,并且計(jì)算len值時(shí)不包含末尾的'\0'
;另外,長(zhǎng)度為零的數(shù)組即char[] buf
不占用內(nèi)存。
其次,我們需要闡述 SDS 數(shù)據(jù)結(jié)構(gòu)的優(yōu)點(diǎn):
2.1.2 常量時(shí)間獲取字符串長(zhǎng)度
對(duì)于傳統(tǒng)的 C 字符串,如果要獲取字符串長(zhǎng)度,例如調(diào)用strlen()
函數(shù),會(huì)從頭開始遍歷字符串直到遇到'\0'
,時(shí)間復(fù)雜度為O(n);
對(duì)于 SDS 數(shù)據(jù)結(jié)構(gòu),能直接訪問(wèn)len屬性獲取字符串長(zhǎng)度,時(shí)間復(fù)雜度為O(1) ,獲取字符串長(zhǎng)度的操作不會(huì)成為 Redis 的性能瓶頸。
2.1.3 避免緩沖區(qū)溢出
傳統(tǒng) C 語(yǔ)言字符串不會(huì)記錄本身長(zhǎng)度,在復(fù)制字符串的時(shí)候,如果分配的內(nèi)存不夠,會(huì)造成緩沖區(qū)溢出。
SDS 復(fù)制字符串時(shí),會(huì)首先檢查 free 變量,判斷內(nèi)存空間是否符合復(fù)制要求,如果不滿足的話,會(huì)將空間擴(kuò)展到所需要的程度,再執(zhí)行復(fù)制操作,不存在緩沖區(qū)溢出的問(wèn)題。
2.1.4 減少內(nèi)存分配次數(shù)
傳統(tǒng) C 語(yǔ)言在對(duì)字符串進(jìn)行修改時(shí),會(huì)對(duì)數(shù)組重新分配內(nèi)存,這個(gè)過(guò)程可能會(huì)涉及操作系統(tǒng)的系統(tǒng)調(diào)用,耗時(shí)較長(zhǎng)。
在 Redis 中, SDS 提供了空間預(yù)分配和惰性空間釋放兩種優(yōu)化規(guī)則:
規(guī)則一:空間預(yù)分配規(guī)則
調(diào)用 SDS 的 API 對(duì) SDS 進(jìn)行修改時(shí),API 不僅會(huì)分配本次修改需要的內(nèi)存空間,還會(huì)分配額外的內(nèi)存空間。
這里遵循的兩種定量分配規(guī)則:
-
sdshdr的len字段值 < 1MB時(shí),會(huì)分配和len同樣大小的未使用空間;
-
sdshdr的len字段值 > 1MB時(shí),會(huì)分配1MB的未使用空間。
其中檢測(cè)是否超過(guò)容量的C語(yǔ)言方法:
static int checkStringLength(client *c, long long size) {
// 字符串長(zhǎng)度為512M,如果超過(guò),直接拋出錯(cuò)誤,結(jié)束函數(shù)
if (size > 512*1024*1024) {
addReplyError(c,"string exceeds maximum allowed size (512MB)");
return C_ERR;
}
return C_OK;
}
如上圖所示,我們?cè)?"mooc"
的基礎(chǔ)上添加了’s’的字符,并且預(yù)留了一個(gè)字符的空間,所以free=1,
規(guī)則二:惰性空間釋放規(guī)則
當(dāng)調(diào)用 SDS API縮短 SDS 字符串時(shí),程序不會(huì)立即回收內(nèi)存空閑的字節(jié),而是使用上述結(jié)構(gòu)體中的free字段將空閑字節(jié)記錄下來(lái),將來(lái)如果有修改操作,則直接使用已分配的空閑內(nèi)存,避免了重新分配。
2.1.5 二進(jìn)制安全
SDS可以存儲(chǔ)任何二進(jìn)制數(shù)據(jù),包括'\0'
,因?yàn)榕袛?SDS 是否達(dá)到字符串終點(diǎn)不是通過(guò)末尾'\0'
判斷,而是通過(guò)len字段值。
傳統(tǒng) C 語(yǔ)言則是遇到'0'
則判定字符串結(jié)束,所以傳統(tǒng)字符串不能存儲(chǔ)圖片、視頻等文件(遇到編碼'\0'
則數(shù)據(jù)會(huì)被截?cái)啵?/p>
3. 小結(jié)
本章節(jié)介紹了 Redis 最基礎(chǔ)的 SDS 數(shù)據(jù)結(jié)構(gòu),有兩點(diǎn)需要重點(diǎn)關(guān)注:
(1)Redis底層對(duì) C 語(yǔ)言自帶數(shù)據(jù)結(jié)構(gòu)進(jìn)行了封裝優(yōu)化。
(2)Redis 作為內(nèi)存數(shù)據(jù)庫(kù),不能避免頻繁的修改和查詢操作,所以在設(shè)計(jì)最初,各種數(shù)據(jù)結(jié)構(gòu)和操作實(shí)現(xiàn)的核心目的就是為了追求速度,遍歷和內(nèi)存分配等耗時(shí)的操作是難以忍受的。