天天看點

[Redis源碼閱讀]sds字元串實作

從開始工作就開始使用Redis,也有一段時間了,但都隻是停留在使用階段,沒有往更深的角度探索,每次想讀源碼都止步在閱讀書籍上,因為看完書很快又忘了,這次逼自己先讀代碼。因為個人覺得寫作需要閱讀文字來增強靈感,那麼寫代碼的,就閱讀更多代碼來增強靈感吧。

redis的實作原理,在《Redis設計與實作》一書中講得很詳細了,但是想通過結合代碼的形式再深入探索,加深自己的了解,現在将自己探索的心得寫在這兒。

sds結構體從4.0開始就使用了5種header定義,節省記憶體的使用,但是不會用到sdshdr5,我認為是因為sdshdr5能儲存的大小較少,2^5=32,是以就不使用它。

其他的結構體儲存了len、alloc、flags以及buf四個屬性。各自的含義見代碼的注釋。

上面可以看到有5種結構體的定義,在使用的時候是通過一個宏來擷取的:

"##"被稱為連接配接符,它是一種預處理運算符, 用來把兩個語言符号(Token)組合成單個語言符号。比如<code>SDS_HDR(8, s)</code>,根據宏定義展開是:

而具體使用哪一個結構體,sds底層是通過flags屬性與<code>SDS_TYPE_MASK</code>做與運算得出具體的類型(具體的實作可見下面的sdslen函數),然後再根據類型去擷取具體的結構體。

在Redis設計與實作一書中講到,相比C字元串而言,sds的特性如下:

常數複雜度擷取字元串長度 杜絕緩沖區溢出 減少記憶體重新配置設定次數 二進制安全

那麼,它是怎麼做到的呢?看代碼。

因為sds将長度屬性儲存在結構體中,是以隻需要讀取這個屬性就能擷取到sds的長度,具體調用的函數時sdslen,實作如下:

可以看到,函數是根據類型調用SDS_HDR宏來擷取具體的sds結構,然後直接傳回結構體的len屬性。

對于C字元串的操作函數來說,如果在修改字元串的時候忘了為字元串配置設定足夠的空間,就有可能出現緩沖區溢出的情況。而sds中的API就不會出現這種情況,因為它在修改sds之前,都會判斷它是否有足夠的空間完成接下來的操作。

拿書中舉例的<code>sdscat</code>函數來看,如果<code>strcat</code>想在原來的"Redis"字元串的基礎上進行字元串拼接的操作,但是沒有檢查空間是否滿足,就有可能會修改了"Redis"字元串之後使用到的記憶體,可能是其他結構使用了,也有可能是一段沒有被使用的空間,是以有可能會出現緩沖區溢出。但是<code>sdscat</code>就不會,如下面代碼所示:

從代碼中可以看到,在執行<code>memcpy</code>将字元串寫入sds之前會調用<code>sdsMakeRoomFor</code>函數去檢查sds字元串s是否有足夠的空間,如果沒有足夠空間,就為其配置設定足夠的空間,進而杜絕了緩沖區溢出。<code>sdsMakeRoomFor</code>函數的實作如下:

sds字元串的很多操作都涉及到修改字元串内容,比如<code>sdscat</code>拼接字元串、<code>sdscpy</code>拷貝字元串等等。這時候就需要記憶體的配置設定與釋放,如果每次操作都配置設定剛剛好的大小,那麼對程式的性能必定有影響,因為記憶體配置設定涉及到系統調用以及一些複雜的算法。

sds使用了空間預配置設定以及惰性空間釋放的政策來減少記憶體配置設定操作。

前面提到,每次涉及到字元串的修改時,都會調用<code>sdsMakeRoomFor</code>檢查sds字元串,如果大小不夠再進行大小的重新配置設定。<code>sdsMakeRoomFor</code>函數有下面這幾行判斷:

函數判斷字元串修改後的大小,如果修改後的長度小于1M,則配置設定給sds的空間是原來的2倍,否則增加1MB的空間。

如果操作後減少了字元串的大小,比如下面的<code>sdstrim</code>函數,隻是在最後修改len屬性,不會馬上釋放多餘的空間,而是繼續保留多餘的空間,這樣在下次需要增加sds字元串的大小時,就不需要再為其配置設定空間了。當然,如果之後檢查到sds的大小實在太大,也會調用<code>sdsRemoveFreeSpac</code>e函數釋放多餘的空間。

二進制安全指的是隻關心二進制化的字元串,不關心具體格式。隻會嚴格的按照二進制的資料存取,不會妄圖以某種特殊格式解析資料。比如遇到'\0'字元不會停止解析。

對于C字元串來說,<code>strlen</code>是判斷遇到'\0'之前的字元數量。如果需要儲存二進制的資料,就不能通過傳統的C字元串來儲存,因為擷取不到它真實的長度。而sds字元串是通過len屬性儲存字元串的大小,是以它是二進制安全的。

在閱讀源碼的過程中,也發現了兩個個人比較感興趣趣的函數:

sdsll2str(将long long類型的整型數字轉成字元串) sdstrim (去除頭部和尾部的指定字元)

我這兩個函數拉出來做了測試,在項目的<code>redis-4.0/tests</code>目錄下。<code>sdstrim</code>函數的實作源碼上面有列出,看看<code>sdsll2str</code>的實作:

函數是通過不斷取餘數,得到原字元串的逆轉形式,接着,通過從尾部開始将字元逐個放到字元串s中,看起來像是一個反轉操作,進而實作了将整型轉為字元串的操作。

覺得感興趣是因為<code>sdsll2str</code>這個函數在之前學習C語言的時候經常能看到作為問題出現,能看到如此簡潔的實作,表示眼前一亮。而在PHP開發時經常使用trim函數,是以想看看它們的差別。

通過詳細地閱讀sds的源碼,不僅學習到sds的實作細節,還學習到了一些常用字元串操作函數的實作。如果隻是僅僅看看資料結構的定義也可以初步了解,但是要深入了解的話還是需要詳細的閱讀具體函數的實作代碼。還是那句,寫代碼的,需要閱讀更多代碼來增強靈感。

我在github有對Redis源碼更詳細的注解。感興趣的可以圍觀一下,給個star。Redis4.0源碼注解。可以通過commit記錄檢視已添加的注解。

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

更多精彩内容,請關注個人公衆号。