1. 程式人生 > >阿里面試這樣問:redis 為什麼把簡單的字串設計成 SDS?

阿里面試這樣問:redis 為什麼把簡單的字串設計成 SDS?

2021開工第一天,就有小夥伴私信我,還給我分享了一道他面阿里的`redis`題(**這傢伙絕比已經拿到年終獎了**),我看了以後覺得挺有意思,題目很簡單,是那種典型的似懂非懂,常常容易被大家忽略的問題。這裡整理出來分享一下,順便自己鞏固一下基礎,希望對正在面試和想要面試的兄弟有點幫助。 題目大致是這樣的 面試官:瞭解`redis`的`String`資料結構底層實現嘛? 鐵子:當然知道,是基於`SDS`實現的 面試官:`redis`是用`C`語言開發的,那為啥不直接用`C`的字串,還單獨設計`SDS`這樣的結構呢? 鐵子:····· ![](https://img-blog.csdnimg.cn/20210130003324468.png) >其實看得出面試官是想看看,鐵子是隻停留在redis的使用層面,還是對底層資料結構有過更深入的研究,面試嘛都愛這樣問大家都懂得。 我們知道`redis`是用`C`寫的,但它卻沒有完全直接使用`C`的字串,而是自己又重新構建了一個叫簡單動態字串`SDS`(simple dynamic string)的抽象型別。 `redis`也支援使用`C`語言的傳統字串,只不過會用在一些不需要對字串修改的地方,比如靜態的字元輸出。 而我們開發中使用`redis`,往往會經常性的修改字串的值,這個時候就會用`SDS`來表示字串的值了。有一點值得**注意**:在redis資料庫中,`key-value`鍵值對含有字串值的,都是由`SDS`來實現的。 比如:在`redis`執行一個最簡單的`set`命令,這時`redis`會新建一個鍵值對。 ``` 127.0.0.1:6379> set xiaofu "程式設計師內點事" ``` 此時鍵值對的`key`和`value`都是一個字串物件,而物件的底層實現分別是兩個儲存著字串`xiaofu`和`程式設計師內點事`的`SDS`結構。 再比如:我向一個列表中壓入資料,redis 又會新建一個鍵值對。 ``` 127.0.0.1:6379> lpush xiaofu "程式設計師內點事" "程式設計師小富" ``` 這時候鍵值對的鍵和上邊一樣,還是一個由SDS實現的字串物件,鍵值對的值是一個包含兩個字串物件的列表物件了,而這兩個物件的底層也是由SDS實現。 ### SDS結構 一個SDS值的資料結構,主要由`len`、`free`、`buf[]`這三個屬性組成。 ```C struct sdshdr{ int free; // buf[]陣列未使用位元組的數量 int len; // buf[]陣列所儲存的字串的長度 char buf[]; // 儲存字串的陣列 } ``` 其中`buf[]`為實際儲存字串的`char`型別陣列;`free`表示buf[]陣列未使用位元組的數量;`len`表示buf[]陣列所儲存的字串的長度。 ![](https://img-blog.csdnimg.cn/20210131172317911.png) 例如上圖表示的是`buf[]`儲存長度為6個位元組的字串,未使用的位元組數`free`為0,但是眼尖的同學會發現這明明是7個字元,還有一個`"\0"`啊? 上邊提到過`SDS`沒有完全直接使用`C`的字串,還是沿用了一些C特性的,比如遵循`C`的字串以空格符結尾的規則,這樣還可以使用一部分C字串的函式。而對於SDS來說,空字串佔用的一位元組是不計算在`len`屬性裡的,會為他分配額外的空間。 簡單瞭解SDS結構後,下邊我們來看看SDS相比於C字串有哪些優點。 ### 效率高 舉個例子:工作中使用`redis`,經常會通過`STRLEN`命令得到一個字串的長度,在`SDS`結構中`len`屬性記錄了字串的長度,所以我們獲取一個字串長度直接取`len`的值,複雜度是O(1)。 ![](https://img-blog.csdnimg.cn/20210209213323972.png) 而如果用C字串,在獲取一個字串長度時,需對整個字串進行遍歷,直至遍歷到空格符結束(C中遇到空格符代表一個完整字串),此時的複雜度是O(N)。 在高併發場景下頻繁遍歷字串,獲取字串的長度很有可能成為`redis`的效能瓶頸,所以SDS效能更好一些。 ### 資料溢位 上邊提到C字串是不記錄自身長度的,相鄰的兩個字串儲存的方式可能如下圖,為字串分配了合適的記憶體空間。 ![](https://img-blog.csdnimg.cn/20210209210915315.png) 如果此時我想把`“程式設計師內點事”`改成`“程式設計師內點事123”`,可之前分配的記憶體只有6個位元組,修改後的字串需要9個位元組才能放下啊,怎麼搞? ![](https://img-blog.csdnimg.cn/20210209213741480.png) 沒辦法只能`侵佔`相鄰字串的空間,自身資料溢位導致其他字串的內容被修改。 而SDS很好的規避了這點,當我們需要修改資料時,首先會檢查當前SDS空間`len`是否滿足,不滿足則自動擴容空間至修改所需的大小,然後再執行修改,如下圖所示。 ![](https://img-blog.csdnimg.cn/20210215210829657.png) **不過有個特殊的地方**,在把`“程式設計師內點事”`的6個位元組擴容到`“程式設計師內點事123”`9個位元組後,發現`free`屬性的值變成了擴容後字串的總長度,這就涉及到下邊要說的記憶體重分配策略了。 ### 記憶體重分配策略 C字串長度是一定的,所以每次在增長或者縮短字串時,都要做記憶體的重分配,而記憶體重分配演算法通常又是一個比較耗時的操作,如果程式不經常修改字串還是可以接受的。 但很不幸,`redis`作為一個數據庫,資料肯定會被頻繁修改,如果每次修改都要執行一次記憶體重分配,那麼就會嚴重影響效能。 SDS通過兩種記憶體重分配策略,很好的解決了字串在增長和縮短時的記憶體分配問題。 #### 1.空間預分配 空間預分配策略用於優化SDS**字串增長**操作,當修改字串並需對SDS的空間進行擴充套件時,不僅會為SDS分配修改所必要的空間,還會為SDS分配額外的未使用空間`free`,下次再修改就先檢查未使用空間`free`是否滿足,滿足則不用在擴充套件空間。 通過空間預分配策略,`redis`可以有效的減少字串連續增長操作,所產生的記憶體重分配次數。 ![](https://img-blog.csdnimg.cn/20210215220737795.png?) 額外分配未使用空間`free`的規則: - 如果對 SDS 字串修改後,`len` 值小於 `1M`,那麼此時額外分配未使用空間 `free` 的大小與`len`相等。 - 如果對 SDS 字串修改後,`len` 值大於等於 `1M`,那麼此時額外分配未使用空間 `free` 的大小為`1M`。 #### 2.惰性空間釋放 惰性空間釋放策略則用於優化SDS**字串縮短**操作,當縮短SDS字串後,並不會立即執行記憶體重分配來回收多餘的空間,而是用`free`屬性將這些空間記錄下來,如果後續有增長操作,則可直接使用。 ![](https://img-blog.csdnimg.cn/20210215221948930.png?) ### 資料格式多樣性 C字串中的字元必須符合某些特定的編碼格式,而且上邊我們也提到,C字串以`\0`空字元結尾標識一個字串結束,所以字串裡邊是不能包含`\0`的,不然就會被誤認是多個。 由於這種限制,使得C字串只能儲存文字資料,像音視訊、圖片等二進位制格式的資料是無法儲存的。 redis 會以處理二進位制的方式操作`Buf`陣列中的資料,所以對存入其中的資料做任何的限制、過濾,只要存進來什麼樣,取出來還是什麼樣。 ### 總結 上邊只是 redis 資料結構的一點基礎知識,沒什麼難度,但以我的面試經驗,如果被問這類問題,不要只含糊其辭的說出底層是SDS,有理有據的把為什麼這樣實現也說出來。 一來可以顯得自己基本功紮實,如果表達的在條理清晰,是個很不錯的加分項;在一個主動打消面試官問下去的念頭,當然就怕不按套路出牌的人! --- >**整理了幾百本各類技術電子書,有需要的同學可以,在我同名公眾號回覆[ 666 ]自取。技術群快滿了,想進的同學可以加我好友,和大佬們一起吹吹技術,期待你的加入**。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020112821073042