Redis使用SDS而不是C語言字符串的原因!
前言
朋友們,我是小許,今天我們聊一聊Redis Sting類型!
Redis為開發(fā)者提供了豐富的數(shù)據(jù)類型,而String類型使用的比較廣泛一種,使用也比較簡(jiǎn)便。
你看用下面命令就可以設(shè)置和獲取Redis字符串值:
redis 127.0.0.1:6379> SET xiaoxu code
OK
redis 127.0.0.1:6379> GET xiaoxu
"code"
Redis 是用 C 語言寫的,但是對(duì)于Redis的字符串,卻不是 C 語言中的字符串(即以空字符’\0’結(jié)尾的字符數(shù)組),它是自己構(gòu)建了一種名為 簡(jiǎn)單動(dòng)態(tài)字符串(simple dynamic string)簡(jiǎn)稱SDS的抽象類型,并將 SDS 作為 Redis的默認(rèn)字符串表示。
圖片
今天的主要內(nèi)容就來說說Redis 什么使用SDS,然后了解String數(shù)據(jù)類型底層數(shù)據(jù)結(jié)構(gòu)、原理和一些注意事項(xiàng)!
Redis 字符串
SDS名為簡(jiǎn)單動(dòng)態(tài)字符串,它是內(nèi)部如何設(shè)計(jì)的,既然是C語言寫的為什么不用C語言的字符串呢?
帶著這些問題我們繼續(xù)往下看!
二進(jìn)制安全性
??♂? 什么是二進(jìn)制安全性?
二進(jìn)制安全是指一種數(shù)據(jù)處理或傳輸?shù)姆绞?,其中?duì)待數(shù)據(jù)的處理不會(huì)受到數(shù)據(jù)中包含的二進(jìn)制數(shù)據(jù)的影響。在計(jì)算機(jī)科學(xué)和編程中,這個(gè)術(shù)語通常與字符串的處理有關(guān)。
?? C語言字符串和Redis SDS的二進(jìn)制安全性問題對(duì)比
C 語言中字符串是以遇到的第一個(gè)空字符 \0 來識(shí)別是否到末尾,因此其只能保存文本數(shù)據(jù),不能保存圖片,音頻,視頻和壓縮文件等二進(jìn)制數(shù)據(jù),否則可能出現(xiàn)字符串不完整的問題,所以其是二進(jìn)制不安全。
Redis SDS(簡(jiǎn)單動(dòng)態(tài)字符串)允許不受限制地存儲(chǔ)和操作任意長(zhǎng)度的二進(jìn)制數(shù)據(jù),保證了二進(jìn)制安全。
C語言字符串的不足
上面我們通過C語言字符串和Redis SDS二進(jìn)制安全性問題的現(xiàn)象對(duì)比,我們知道了C語言字符串只能保存文本數(shù)據(jù),不能保存圖片,音頻,視頻和壓縮文件等二進(jìn)制數(shù)據(jù)。
與Redis的SDS比起來有以下不足:
- ? 獲取字符串長(zhǎng)度的時(shí)間復(fù)雜度為 n
- ? API是不安全的可能造成緩沖區(qū)溢出
- ? 只能保存文本數(shù)據(jù)
SDS結(jié)構(gòu)
現(xiàn)在開始進(jìn)入正題,挖一挖Redis String的底層實(shí)現(xiàn)!
我們復(fù)制了其中一種SDS類型【sdshdr8】,它在Redis源碼中的結(jié)構(gòu)代碼如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
字段說明:
- ? len : 記錄buf數(shù)組中已使用的字節(jié)數(shù)量
- ? alloc : 分配的buf數(shù)組長(zhǎng)度,不包括頭和空字符結(jié)尾
- ? flags : 標(biāo)志位,標(biāo)記當(dāng)前字節(jié)數(shù)組是 sdshdr8/16/32/64 中的哪一種,占 1 個(gè)字節(jié)。
- ? buf[] : 字符數(shù)組,用于存放實(shí)際字符串
圖片
定義的這些字段有以下一些好處:
- ? 用單獨(dú)的變量 len 和 free,可以方便地獲取字符串長(zhǎng)度和剩余空間;
- ? 內(nèi)容存儲(chǔ)在動(dòng)態(tài)數(shù)組 buf 中,SDS 對(duì)上層暴露的指針指向 buf,而不是指向結(jié)構(gòu)體 SDS。因此,上層可以像讀取 C 字符串一樣讀取 SDS 的內(nèi)容,兼容 C 語言處理字符串的各種函數(shù),同時(shí)也能通過 buf 地址的偏移,方便地獲取其他變量;
- ? 讀寫字符串不依賴于
\0
,保證二進(jìn)制安全。
對(duì)應(yīng)在文章開頭中我們?cè)O(shè)置的 key="xiaoxu"、value="code",存儲(chǔ)情況如下圖所示:
圖片
從圖中可以看出SDS 也遵循 C 字符串以空字符“\0”結(jié)尾的慣例,而保存空字符的大小不計(jì)算在 SDS 的 len 屬性中。
不過你也注意到了此時(shí)表示SDS類型的flags字段的值是 1,也就是 sdshdr8。
SDS類型
在SDS結(jié)構(gòu)一節(jié)中我們使用的是sdshdr8,而Redis 3.2 版本之后,SDS 由一種數(shù)據(jù)結(jié)構(gòu)變成了 5 種數(shù)據(jù)結(jié)構(gòu)。
??這5 種類型分別是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64
五種類型的區(qū)別在于數(shù)組的 len 長(zhǎng)度和分配空間長(zhǎng)度 alloc。
圖片
? sdshdr5:存儲(chǔ)大小為 32 byte = 2^ 5 【被棄用】
? sdshdr8:存儲(chǔ)大小為 256 byte = 2^ 8
? sdshdr16:存儲(chǔ)大小為 64KB = 2 ^16
? sdshdr32:存儲(chǔ)大小為 4GB = 2^ 32
? sdshdr64:存儲(chǔ)大小為 2^ 64
圖片
上面5 種數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)不同長(zhǎng)度的內(nèi)容,而在使用中Redis 會(huì)根據(jù) SDS 存儲(chǔ)的內(nèi)容長(zhǎng)度來選擇不同的結(jié)構(gòu)。
底層編碼選擇
字符串是 Redis最基本的數(shù)據(jù)類型,Redis 中字符串對(duì)象的編碼可以是下面三種類型:
圖片
? int 編碼:存儲(chǔ)8個(gè)字節(jié)的長(zhǎng)整型(long,2^63-1)字符串,長(zhǎng)度小于等于20
? embstr 編碼:長(zhǎng)度小于44字節(jié)的字符串
? raw 編碼:長(zhǎng)度大于44字節(jié)的字符串
?
講了半天理論還比不上一個(gè)案例,這里舉個(gè)栗子:
以下案例截取自網(wǎng)絡(luò)
圖片
從圖中我們可以可以發(fā)現(xiàn),當(dāng)輸入純數(shù)字字符串的時(shí)候,采用的是 int 編碼,而字符串小于等于 44 則為 embstr,大于 44 則為 raw 編碼
注:編碼轉(zhuǎn)換在Redis寫入數(shù)據(jù)時(shí)完成,且轉(zhuǎn)換過程不可逆,只能從小內(nèi)存編碼向大內(nèi)存編碼轉(zhuǎn)換
?? embstr和raw之間有什么區(qū)別?
embstr:只分配一次內(nèi)存空間,SDS結(jié)構(gòu)體和RedisObject分配在同一塊連續(xù)的內(nèi)存空間
raw:需要分配兩次內(nèi)存空間,SDS結(jié)構(gòu)體和依賴RedisObject不在連續(xù)
圖片
SDS相對(duì)C字符串的好處
SDS 是Redis中用于存儲(chǔ)二進(jìn)制數(shù)據(jù)的一種結(jié)構(gòu), 具有動(dòng)態(tài)擴(kuò)容的特點(diǎn)。
使用它主要有以下好處:
? 讀取字符串長(zhǎng)度快:獲取 SDS 字符串的長(zhǎng)度只需要讀取 len 屬性,時(shí)間復(fù)雜度為 O(1)
? 杜絕緩沖區(qū)溢出:SDS 數(shù)據(jù)類型,在進(jìn)行字符修改的時(shí)候,會(huì)首先根據(jù)記錄的 len 屬性檢查內(nèi)存空間是否滿足需求
? 二進(jìn)制安全:SDS 的API 都是以處理二進(jìn)制的方式來處理 buf 里面的元素,并且 SDS 不是以空字符串來判斷是否結(jié)束
? 減少內(nèi)存重新分配次數(shù):對(duì)于修改字符串SDS實(shí)現(xiàn)了空間預(yù)分配和惰性空間釋放兩種策略
這些好處也就解釋了為什么Redis要使用SDS來實(shí)現(xiàn)字符串了。
文末提問
1:SDS實(shí)際能存儲(chǔ)多大字符串?
SDS 結(jié)構(gòu)中 alloc字段 表示允許容納的最大字符長(zhǎng)度,而類型為sdshdr32的存儲(chǔ)大小為 4GB,但是現(xiàn)實(shí)并不是這樣的。
Redis的文檔和源代碼中寫死它的字符串最大長(zhǎng)度為512M,超過這個(gè)長(zhǎng)度將報(bào)錯(cuò)
static int checkStringLength(client *c, long long size) {
if (size > 512*1024*1024) {
addReplyError(c,"string exceeds maximum allowed size (512MB)");
return C_ERR;
}
return C_OK;
}
那為什么在Redis中會(huì)設(shè)置這個(gè)限制呢?我覺得可能還有如下考慮
- ? 程序中一般不會(huì)有那么大的數(shù)據(jù)量存入緩存
- ? 大的數(shù)據(jù)量對(duì)網(wǎng)絡(luò)和性能有一定影響
2:SDS如何空間預(yù)分配和惰性空間釋放?
Redis的SDS,由于len屬性和alloc屬性的存在,對(duì)于修改字符串SDS實(shí)現(xiàn)了空間預(yù)分配和惰性空間釋放兩種策略:****
? 空間預(yù)分配:對(duì)字符串進(jìn)行空間擴(kuò)展的時(shí)候,擴(kuò)展的內(nèi)存比實(shí)際需要的多,這樣就不需要每次增大字符串都需要分配空間,減少了內(nèi)存重分配的次數(shù)
? 惰性空間釋放:對(duì)字符串進(jìn)行縮短操作時(shí),程序空余出來的空間并不會(huì)直接釋放,而是會(huì)被保留,等待下次再次使用
3:attribute ((packed))是什么?
在Redis SDS定義的五種結(jié)構(gòu)體類型中有一個(gè) attribute ((packed)) 關(guān)鍵字聲明
圖片
attribute ((packed)) 的作用就是告訴編譯器取消結(jié)構(gòu)在編譯過程中的優(yōu)化對(duì)齊,按照實(shí)際占用字節(jié)數(shù)進(jìn)行對(duì)齊。
Redis SDS默認(rèn)情況下是按sdshdr8(8字節(jié)來分配),而經(jīng)過__attribute__ ((packed)) 定義結(jié)構(gòu)體,目的就是讓編譯器按照實(shí)際占用來分配內(nèi)存空間。