我們知道,Redis 支持字串、哈希、串列、集合和有序集合五種基本型別,那么我們如何把圖片、音頻、視頻或者壓縮檔案等二進制資料保存到 Redis 中呢?之前在使用 Memcached 快取這類資料時是把它們轉換成 Base64 字串后再進行保存的,在 Redis 中也可以使用同樣的方式,但是,Redis 中的 字串是支持直接存盤二進制資料的,那么我們就聊聊他是如何實作的?
簡單動態字串 ( SDS )
Redis 是使用 C 語言撰寫的,但是,Redis 沒有直接使用 C 語言自有的字串型別,而是自己構建了一個稱為簡單動態字串 ( simple dynamic string, SDS ) 的抽象型別,你可能會有個疑惑,Redis 為什么要構建自己的 SDS,而不適用 C 語言自有的字串?SDS 長什么樣?使用 SDS 又有什么優點呢?
C 語言中的字串
C 語言中傳統字串是使用長度為 N+1 的字符陣列來表示長度為 N 的字串,并且字串陣列的最后一個元素總是空字符 '\0',如下所示:

C 語言中的這種簡單的字串表示方式,不能滿足 Redis 對字串在安全性、效率以及功能方面的要求,所以 Redis 自己構建了字串表示方式 SDS,
SDS 的定義
在 Redis 原始碼檔案 sds.h 中定義了 SDS 的結構,如下所示:
struct sdshdr {
// 記錄 buf 陣列中已使用的數量
unsigned int len;
// 記錄 buf 中未使用的空間數量
unsigned int free;
// 字符陣列,用于保存字串
char buf[];
};
有上面的原始碼,我們看到 SDS 中也是使用字符陣列來存放字串,但是,它是通過 屬性 len 來表示當前字串的長度,
SDS 與 C 字串的區別
接下來我們就通過對比 SDS 和 C 字串的區別,來說明為什么 Redis 需要構建 SDS,而不使用 C 字串?
1、獲取字串長度的復雜度
因為 C 字串不記錄自身的長度,所以在獲取一個 C 字串的長度時,需要遍歷整個字符陣列,對遇到的每個字符進行計數,知道遇到代表字串結尾的空字串'\0' 為止,那么,這個操作的復雜度為 O(N),
上面的 SDS 結構的原始碼,我們可知,SDS 字串通過 len 記錄了當前字串的長度,那么當獲取 SDS 的字串長度的復雜度僅為 O(1),這就確保了獲取字串長度的作業不會成為 Redis 的性能瓶頸,因為不管字串多長,其獲取長度的復雜度都是O(1),這或許也是 Redis 快的一個原因吧,
2、杜絕緩沖區溢位
我們在日常 Java 開發中,經常會用到字串的拼接,在 C 語言 和 Redis 中也經常用到字串的拼接,由于 C 字串不記錄自身長度,這樣就帶來了一個問題就是,如果在拼接字串時,如果記憶體計算不當,就會造成緩沖區溢位,如下所示:
開始為 相鄰著的字串 “Redis” 和 “Mysql”

假如,我們把 “Redis” 改為 “Redis Client”,但忘記了重新分配記憶體,就會導致如下所示的結果,

可以看到,我們修改 “Redis” 字串時,無意導致 把 “Mysql” 字串的位置給占了,導致資料污染,那么 Redis 的 SDS 字串是如何解決的呢?
由 SDS 的結構我們可知,SDS 有 len 存盤了當前長度,還有 free 存盤了未使用的長度,這樣就簡單多了,當操作字串拼接時,可以先判斷一下 free 和需要拼接的字串,是否能夠存的下,如放的下則直接執行,如果放不下,則進行擴容操作,
3、減少修改字串時帶來的記憶體分配次數
正如我們前面介紹的,因為 C 字串底層是字串陣列,每次創建總是一個 N+1 個字符長的陣列( 額外的一個字符空間用于保存空字符,這也是一個坑),
Redis 是個高性能的記憶體資料庫,如果需要對字串進行頻繁的拼接和截取操作,如果我們忘了重新分配空間,就會造成緩沖區溢位,
因為記憶體重分配涉及復雜的演算法,并且可能需要執行系統呼叫,所以它通常是一個比較耗時的操作,在 Redis 這種對于速度要求嚴苛,資料頻繁修改的資料庫中,這種耗時操作是應該避免的,
Redis 為了避免 C 字串的這種缺陷,SDS 通過未使用空間解除了字串長度和底層陣列長度之間的關聯,SDS實作了空間預分配和惰性空間釋放兩種優化策略,去達到性能最大化,空間利用最大化:
-
空間預分配
當對 SDS 進行修改,并且需要進行空間擴展操作的時候,Redis 程式不僅會為 SDS 分配修改所必須要的空間,并且根據特定的公式,分配額外的空間,這樣就可以避免我們連續執行字串添加所帶來的記憶體分配消耗,
比如有如下字串:

我們執行拼接函式,將SDS的長度修改為13位元組,并將SDS的未使用空間同樣修改為13位元組

如果我們在拼接字串"abc", 長度為 9 小于 free 的值13,所以無需再次執行空間分配操作,

-
空間惰性釋放
剛才說到了分配空間時,會預分配多余的空間,你可以會問這個會不會導致記憶體泄露呢?這個無需擔心,當我們執行完一個字串縮減的操作,Redis 并不會馬上識訓我們的空間,因為可以預防你繼續添加的操作,這樣可以減少分配空間帶來的消耗,但是當你再次操作還是沒用到多余空間的時候,Redis 也還是會識訓對于的空間,防止記憶體的浪費的,
比如我們有如下字串:

我么截取字串,洗掉“ Client”,結果如下,我們注意,洗掉字串的空間并沒有洗掉,如果后續還是用就可以繼續使用,不用再次分配空間,如果不使用了,呼叫函式洗掉即可,

4、二進制安全
我們上文提的 C 字串中是以空字符 ‘\0’ 來表示 C 字串的終止符號,這樣字串里就不能包含空字符,否則最先被讀入的空字符將被誤認為字串傳結尾,這些限制使得 C 字串只能保存文本資料,而不能保存像圖片、音頻、視頻、壓縮檔案等這樣的二進制資料,如下所示:

在 Redis 中 SDS 中就不存在此問題,因為在 SDS 中通過 len 屬性保存了字串的長度,所以,在 SDS 中是通過 len 屬性的值而不是空字符來判斷字串的是否結束,上圖使用 SDS 存盤如下所示:

通過使用二進制安全的 SDS,而不是 C 字串,使得Redis不僅可以保存文本資料,還可以保存任意格式的二進制資料,
總結
Redis 通過自構建 SDS,而不使用 C 字串,不僅解決了 C 字串存在的緩沖區溢位問題,同時,還通過減少因修改字串導致的頻繁分配記憶體空間和獲取長度導致的性能消耗,再者,由于 SDS 不再使用空字符 '\0' 標志字串的結尾,使得 Redis 不但可以存盤文本資料,還可以保存任意格式二進制資料,
參考: 《Redis 設計與實作》
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/273660.html
標籤:其他
