redis 沒有直接使用c 語言傳統的字串表示(以空字元結尾的字元陣列,以下簡稱c字串),而是自己構建了一種名為簡單動態字串( ****** dynamic string, sds )的抽象型別,並將sds 用作redis 的預設字串表示。
在redis 裡面, c 字串只會作為字串字面量( s回ng literal )用在一些無須對字串值進行修改的地方,比如列印日誌。
redislog(redis_warning,」redis is now ready to exit, bye bye ...」) ;
當redis 需要的不僅僅是乙個字串字面量,而是乙個可以被修改的字串值時, redis就會使用sds 來表示字串值,比如在redis 的資料庫裡面,包含字串值的鍵值對在底層都是由sds 實現的。
如果客戶端執行命令:redis> set msg 「hello world」
那麼redis 將在資料庫中建立乙個新的鍵值對,其中:
鍵值對的鍵是乙個字串物件,物件的底層實現是乙個儲存著字串」msg」的sds。
鍵值對的值也是乙個字串物件,物件的底層實現是乙個儲存著字串」hello world 」的sds 。
那麼redis 將在資料庫中建立乙個新的鍵值對,其中:
鍵值對的鍵是乙個字串物件,物件的底層實現是-鬥儲存了字串」fruits 」的sds。
除了用來儲存資料庫中的字串值之外, sos 還被用作緩衝區( buffer ) : aof 模組中的aof 緩衝區,以及客戶端狀態中的輸人緩衝區,都是由sds 實現的。
每個sds.h/sdshdr 結構表示乙個sds 值:
sds 遵循c 字串以空字元結尾的慣例,儲存空字元的1 位元組空間不計算在sds 的len 屬性裡面,並且為空字元分配額外的1 位元組空間,以及新增空字元到字串末尾等操作,都是由sds 畫數自動完成的,所以這個空字元對於sds 的使用者來說是完全透明的。遵循空字元結尾這一慣例的好處是, sds 可以直接重用一部分c 字串函式庫裡面的畫數。舉個例子,如果我們有乙個指向圖2-1 所示sds 的指標s ,那麼我們可以直接使用
printf(「%d」, s->buf)
根據傳統, c 語言使用長度為n+l 的字元陣列來表示長度為n 的字串,並且字元陣列的最後乙個元素總是空字元』\0』。 c 語言使用的這種簡單的字串表示方式,並不能滿足redis 對字串在安全性、效率以及功能方面的要求。
因為c 字串並不記錄自身的長度資訊,所以為了獲取乙個c 字串的長度,程式必須遍歷整個字串,對遇到的每個字元進俯+數,直到遇到代表字串結尾的空字元為止,這個操作的複雜度為o(n)的。和c 字串不同,因為sds 在len 屬性中記錄了sds 本身的長度,所以獲取乙個sds 長度的複雜度僅為o(1)。
通過使用sds 而不是c 字串, redis 將獲取字串長度所需的複雜度從o(n) 降低到了o(1),這確保了獲取字串長度的工作不會成為redis 的效能瓶頸。例如,因為字串鍵在底層使用sds 來實現,所以即使我們對乙個非常長的字串鍵反覆執行strlen命令,也不會對系統效能造成任何影響,因為st.缸,en命令的複雜度僅為o(1)。
c 字串不記錄自身長度帶來的另乙個問題是容易造成緩衝區溢位( buffer overflow )。因為c 字串不記錄自身的長度,所以c語言api strcat 方法假定使用者在執行這個函式時,已經為dest 分配了足夠多的記憶體,可以容納src 字串中的所有內容,而一旦這個假定不成立時,就會產生緩衝區溢位。
sds 的空間分配策略完全杜絕了發生緩衝區溢位的可能性:當sds api 需要對sds 進行修改時, api 會先檢查sds 的空間是否滿足修改所需的要求,如果不滿足的話, api 會自動將sds 的空間擴充套件至執行修改所需的大小,然後才執行實際的修改操作,所以使用sds 既不需要手動修改sds 的空間大小,也不會出現前面所說的緩衝區溢位問題。
因為c 字串並不記錄自身的長度,所以對於乙個包含了n 個字元的c 字串來說,這個c 字串的底層實現總是乙個n+l 個字元長的陣列(額外的乙個字元空間用於儲存空字元)。因為c 字串的長度和底層陣列的長度之間存在著這種關聯性,所以每次增長或者縮短乙個c 字串,程式都總要對儲存這個c 字串的陣列進行一次記憶體重分配操作:
如果程式執行的是縮短字串的操作,比如截斷操作( trim ),那麼在執行這個操作之後,程式需要通過記憶體重分配來釋放字串不再使用的那部分空間一一如果忘了這一步就會產生記憶體洩漏。
因為記憶體重分配涉及複雜的演算法,並且可能需要執行系統呼叫,所以它通常是乙個比較耗時的操作:
在一般程式中,如果修改字串長度的情況不太常出現,那麼每次修改都執行一次記憶體重分配是可以接受的。
但是redis 作為資料庫,經常被用於速度要求嚴苛、資料被頻繁修改的場合,如果每次修改宇符串的長度都需要執行一次記憶體重分配的話,那麼光是執行記憶體重分配的時間就會占去修改字串所用時間的一大部分,如果這種修改頻繁地發生的話,可能還會對效能造成影響。
為了避免c 字串的這種缺陸, sds通過未使用空間解除了字串長度和底層陣列長度之間的關聯:在sds 中, buf 陣列的長度不一定就是字元數量加一,陣列裡面可以包含未使用的位元組,而這些位元組的數量就由sds 的free 屬性記錄。通過未使用空間, sds 實現了空間預分配和惰性空間釋放兩種優化策略。
通過未使用空間, sds實現了空間預分配和惰性空間釋放兩種優化策略。
空間預分配:
空間預分配用於優化sos 的字串增長操作:當sos 的api 對乙個sds 進行修改,並且需要對sos 進行空間擴充套件的時候,程式不僅會為sds 分配修改所必須要的空間,還會為sds分配額外的未使用空間。其中,額外分配的未使用空間數量由以下公式決定:
如果對sds 進行修改之後, sds 的長度(也即是len 屬性的值)將小於1mb,那麼程式分配和len 屬性同樣大小的未使用空間,這時sds len 屬性的值將和free 屬性的值相同。舉個例子,如果進行修改之後, sds 的len 將變成13 位元組,那麼程式也會分配13 位元組的未使用空間, sds 的buf 陣列的實際長度將變成13+13+1=27 位元組(額外的一位元組用於儲存空字元)。
如果對sds 進行修改之後, sds 的長度將大於等於1mb,那麼程式會分配lmb 的未使用空間。舉個例子,如果進行修改之後, sds 的len 將變成30mb,那麼程式會分配1mb的未使用空間, sds 的buf 陣列的實際長度將為30 mb+ 1mb+ lbyte 。
在擴充套件sds 空間之前, sdsapi 會先檢查未使用空間是否足夠,如果足夠的話, api就會直接使用未使用空間,而無須執行記憶體重分配。通過這種預分配策略, sds 將連續增長n 次字串所需的記憶體重分配次數從必定n 次降低為最多n 次。
惰性空間釋披
惰性空間釋放用於優化sds 的字串縮短操作:當sds 的api 需要縮短sds 儲存的字串時,程式並不立即使用記憶體重分配來**縮短後多出來的位元組,而是使用free 屬性將這些位元組的數量記錄起來,並等待將來使用。與此同時, sds 也提供了相應的a凹,讓我們可以在有需要時,真正地釋放sds 的未使用空間,所以不用擔心惰性空間釋放策略會造成記憶體浪費。
雖然資料庫一般用於儲存文字資料,但使用資料庫來儲存二進位制資料的場景也不少見,因此,為了確保redis 可以適用於各種不同的使用場景, sds 的api 都是二進位制安全的(binary-safe ),所有sds api 都會以處理二進位制的方式來處理sds 存放在buf陣列裡的資料,程式不會對其中的資料做任何限制、過濾、或者假設,資料在寫入時是什麼樣的,它被讀取時就是什麼樣。這也是我們將sds 的buf 屬性稱為位元組陣列的原因——redis 不是用這個陣列來儲存字元,而是用它來儲存一系列二進位制資料。通過使用二進位制安全的sds ,而不是c 字串,使得redis 不僅可以儲存文字資料,還可以儲存任意格式的二進位制資料。
雖然sds 的api 都是二進位制安全的,但它們一樣遵循c 字串以空字元結尾的慣例:這些api 總會將sds 儲存的資料的末尾設定為空字元,並且總會在為buf 陣列分配空間時多分配乙個位元組來容納這個空字元,這是為了讓那些儲存文字資料的sds 可以重用一部分
Redis 設計與實現
本書的目標是以簡明易懂的方式講解 redis 的內部執行機制,通過閱讀本書,你可以了解到 redis 從資料結構到伺服器構造在內的幾乎所有知識。為了保證內容的簡潔性,本書會盡量以高抽象層次的角度來觀察 redis 並將 的細節留給讀者自己去考究。如果讀者只是對 redis 的內部運作機制感興趣,但並...
redis設計與實現
物件所使用的底層資料結構 編碼常量 object encoding 命令輸出 整數redis encoding int int embstr編碼的簡單動態字串 sds redis encoding embstr embstr 簡單動態字串 redis encoding raw raw 字典redis...
《redis設計與實現》
提高系統架構的效能,增加快取層是常見的優化方式,redis和memcache是當前採用較多的快取元件,redis被稱為資料結構資料庫 redis和memcache的區別 redis和memcahce都是基於記憶體的key value儲存 memcache的事件模型是多執行緒reactor模型,效能更...