一、Redis基本操作——String(原理篇)


小喵的嘮叨話:最近京東圖書大減價,小喵手癢了就買了本《Redis設計與實現》[1]來看看。這里權當小喵看書的筆記啦。這一系列的模式,主要是先介紹Redis的實現原理(可能很大一部分會直接照搬原作者的描述),加上小喵自己的想法,之后配合Redis官網上的各種相關的操作命令(原書上貌似沒有很多的介紹命令)。

 

小喵的個人博客地址: http://miaoerduo.com, 隨時歡迎各位的大駕。

原文鏈接: http://www.miaoerduo.com/redis/redis基本操作-string原理.html

排版比這里的要好看一點(小喵可是裝了很多美化插件的)。

 

本章介紹Redis中最常用到的字符串(String)。

Redis的字符串(String)的實現

小喵之前有看到過《Redis設計與實現》的一部分章節。這是第一章的內容,小喵也是因為看了這一章的內容,才決定要買本仔細研究的。

首先,我們知道Redis是由C語言編寫的,以高效和輕量著稱。而C語言中的字符串是怎么實現的呢?字符數組。

比如一個簡單的字符串”hello world”,其實是一個如下的字符的數組:

[‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘ ‘, ‘w’, ‘o’, ‘r’, ‘l’, ‘d’, ‘\0’]

最后的一個’\0’是空字符,表示字符串的結尾。

 

Redis由於各種原因,並沒有直接使用了C語言的字符串結構,而是對其做了一些封裝,得到了自己的簡單動態字符串(simple dynamic string, SDS)的抽象類型。Redis中,默認以SDS作為自己的字符串表示。只有在一些字符串不可能出現變化的地方使用C字符串。

 

SDS的定義如下:

可以看出來,SDS的結構並不復雜。

buf是一塊可用的內存空間,通常大小會大於等於需要存儲的字符串的大小(大於?為什么要大於呢?讀者可以思考一下)。

len表示字符串的長度,也表示buf中已經被使用的空間的大小。

free表示buf中沒有被使用的空間的大小。

要注意的是,buf的大小等於len+free+1,其中多余的1個字節是用來存儲’\0’的。

 

那么這么封裝到底有什么好處呢?我們一點一點剖析。

1,常數復雜度獲取字符串長度

在C語言中的字符串只是簡單的字符的數組,當使用strlen獲取字符串長度的時候,C語言內部其實是直接順序遍歷數組的內容,找到對應的’\0’對應的字符,從而計算出字符串的長度。顯然這個算法復雜度和字符串的長度成正比,即O(N)。而對於SDS來說,只需要訪問SDS的len屬性就能得到字符串的長度,復雜度為O(1)。這樣,獲取字符串長度的操作就不會成為Redis的瓶頸(當然len的作用不止這么簡單,后面還會介紹別的)。

2,杜絕緩沖區溢出

我們知道C++里面的字符串使用了STL的string類型,我們開發者不太需要關注內存的分配和釋放的過程。但是Redis是C語言編寫的,並沒有這么方便的數據類型。對於字符串的拼接、復制等操作,C語言開發者必須確保目標字符串的空間足夠大,不然就會出現溢出的情況。

上面的三句代碼,就是C語言的字符串拼接和復制的使用,但是明顯出現了緩沖區溢出的問題。字符數組a的長度是10,而”hello world”字符串的長度為11,則需要12個字節的空間來存儲(不要忘記了’\0’)。

然后,我們看看Redis的SDS是怎么處理字符串修改的這種情況。

當使用SDS的API對字符串進行修改的時候,API內部第一步會檢測字符串的大小是否滿足。如果空間已經滿足要求,那么就像C語言一樣操作即可。如果不滿足,則拓展buf的空間,使得滿足操作的需求,之后再進行操作。每次操作之后,len和free的值會做相應的修改。

這就是SDS的全部的高明之處了嗎?當然不!

當API發現SDS的buf的容量不夠的時候,並不是簡單申請正好適合的大小,而是額外申請了一倍的空間!我們以sds的API sdscat函數為例,該函數實現了sds的拼接的功能。

下面的例子是”hello” 和” world”的拼接的過程。

修改之前的sds

圖1 sdscat執行之前的sds

修改之后的sds

圖2 sdscat執行之后的sds

這里的buf的容量是23(free + len + 1)。為什么要這么做呢?耐心向下看吧。

3,減少修改字符串時帶來的內存重分配次數

我們之前說到,對於一個N長的字符串,C語言中底層是一個N+1長的字符數組(有一個字節存放空字符)。C字符串的長度和底層數組之間的長度存在着這樣的關系,因此當進行字符串的操作而導致字符串長度發生變化的時候,需要對內存進行重新分配。

如果操作會增長字符串,那么在執行之前,就需要進行內存分配擴充底層數組的大小。如果是縮短字符串的操作,則需要釋放額外的內存(這是書中的意思,但小喵覺得如果字符串縮小的話,其實並不用立刻釋放內存,如果字符串是malloc出來的話,需要釋放的直接free就可以,也不需要給定空間的大小,所以不會出現內存泄露。當然,也可能Redis里面是用別的方式實現,這樣小喵就不懂了)。

對於一般的程序而言,如果修改字符串的操作並不是特別常出現,那么每次修改都重新分配一下內存也是可以接受的。但是Redis作為一個數據庫,其讀寫速度,數據修改頻率都被要求達到很高的效率。因此這種低效的方式並不適合Redis。

為了避免C字符串的這些弊端,SDS通過未使用空間解除了字符串長度和底層數組長度之間的關系。也就是之前說的buf的長度為len和free之和(再加1)。數字里面可以包含未使用的空間,大小用free表示。

Redis主要通過以下兩種策略來處理內存問題。

i) 空間預分配

這種方式用於處理字符串長度增加的問題。如果對字符串的修改使得字符串的長度增加,API首先會判斷buf的空間大小是否滿足,如果滿足則直接操作,如果不滿足,則進行如下操作:

如果對SDS進行修改之后的,SDS的長度(即len的值)小於1MB。程序將額外分配和len一樣大小的未使用空間。以上面的”hello” + ” world”的操作為例。在這個例子中”hello”的len是5(不考慮’\0′),修改之后的字符串”hello world”長度為11,那么新的SDS的buf的容量就是11*2+1。其中len和free都是11,多余的1字節用來存儲’\0’。

如果對SDS修改之后的長度大於1MB,那么程序會分配1MB的未使用空間。比如原數據是5MB,修改之后需要6MB的空間,進行修改的操作后,buf的實際空間應該是7MB,其中len為6MB,free為1MB。

那么這些未使用空間能夠做什么呢?為什么根據SDS的修改會的大小會有兩種不同的分配原則呢?

小喵是這么認為的,如果數據被更改,則說明這個數據很可能會被再次更改,如果能夠提前分配多余的空間,那么下一次變化的時候很可能就不需要再次分配空間了。如果數據比較小(<1MB)的時候,可以分配等大的未使用空間。但是如果數據已經很大的時候(>1MB),再分配同等大小的內存會顯得十分浪費,畢竟不能確保這個字符串一定會被再次修改,所以只額外分配1MB的空間。

通過這種策略,SDS可以做到N次修改,最多進行N次內存分配。而C字符串在N次修改則一定要進行N次內存分配。一個是至多N次,一個是一定N次。用小喵的腦袋想,也覺得SDS這個策略簡單、粗暴、高效。

ii) 惰性空間釋放

當執行字符串長度縮短的操作的時候,SDS並不直接重新分配多出來的字節,而是修改len和free的值(len相應減小,free相應增大,buf的空間大小不變化)。通過惰性空間釋放,可以很好的避免縮短字符串需要的內存重分配的情況。而且多余的空間也可以為將來可能有的字符串增長的操作做優化。

當然,SDS也提供直接釋放未使用空間的API,在需要的時候,也能真正的釋放掉多余的空間。

4,二進制安全

C字符串中的字符必須符合某種編碼(比如ASCII),並且字符串除了末尾之外不能出現空字符,否則會被程序認為是字符串的結尾。這就使得C字符串只能存儲文本數據,而不能保存圖像,音頻等二進制數據。(這里,小喵的觀點是不同的,小喵本人是做圖像的,opencv等的庫,都是使用unsigned char*來存儲圖像的數據。我們完全可以把字符數組看成一堆內存,存放任何數據都可以)

使用SDS就不需要依賴控制符,而是用len來指定存儲數據的大小。同時所有的SDS操作的API都是二進制安全的(binary-safe),所有的SDS API都會以處理二進制的方式來處理SDS的buf的數據。程序不會對buf的數據做任何限制、過濾或假設,數據寫入的時候是什么,讀取的時候依然不變。

這也是我們將SDS的buf屬性程序字節數組的原因,Redis不是使用這個數組來保存字符,而是儲存一系列二進制數據。

5,兼容部分C字符串函數

由於SDS的buf的定義和C字符串完全相同,因此很多的C字符串的操作都是適用於SDS->buf的。比如當buf里面存的是文本字符串的時候,printf函數,也完全可以試用。這樣,Redis就不需要為所有的字符串的處理編寫自己的函數,大多數通過調用C語言的函數就可以。

總結

 

C字符串 SDS
獲取字符串長度的復雜度為O(N) 獲取字符串長度的復雜度為O(1)
API是不安全的,可能會造成緩沖區溢出 API是安全的,不會造成緩沖區溢出
修改字符串長度N次必然需要執行N次內存重分配 修改字符串長度N次最多需要執行N次內存重分配
只能保存文本數據 可以保存文本或者二進制數據
可以使用所有庫中的函數 可以使用一部分庫的函數

以上則是Redis的string結構的原理部分。下一章我們會介紹一些string操作的redis命令。

轉載請注明出處。

參考:

[1]http://redisbook.com/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM