蜻蜓點水說說Redis的String的奧祕

本篇博客參考:掘金Redis小冊 敖丙

如果面試官問你,單線程的Redis爲什麼那麼快,你可能脫口而出,因爲單線程,避免上下文切換;因爲基於內存,比硬盤讀寫快很多;因爲採用的是多路複用網絡模型。不管你是否真的理解了,這個回答足以應付一半以上的面試官了,但是如果可以再進行補充就更好了:因爲Redis對各種數據結構進行了精心的設計,比如String採用的是SDS,比如list採用的是ziplist,quicklist等等,可能這樣的回答就比較出彩了,至少可以說出部分面試者不太清楚的事情。今天我們就來看看Redis中最常用的String數據結構的奧祕。

從位操作說起

bitmap的應用場景很多,比如大名鼎鼎的布隆過濾器(之前的博客有介紹過:《大白話布隆過濾器》),比如統計指定用戶在一年內任意日期內的登錄情況,統計任意日期內,所有用戶的登錄情況等等,都可以用bitmap來實現(之前的博客也有介紹過:《有點長的博客:Redis不是隻有get set那麼簡單》),所以好好看看bitmap還是很有必要的,不過本篇博客不打算詳細介紹bitmap,只是通過bitmap引出我們今天的話題,而bitmap的核心就是位操作。

如果我們要往Redis塞入一個value爲“hello”的key,這個所有人都會:

test:1>set key hello
"OK"
test:1>get key
"hello"

如果我們要利用位操作實現這個需求呢?什麼,我沒聽錯把,位操作也可以實現這個需求嗎?當然可以,因爲在Redis中,String就是用byte數組來存儲的。

什麼,你不信?那請繼續看下去。

要用位操作實現這個需求,我們要獲得“hello”的ascii碼,接着計算出二進制:

比如,“h”的ascii碼是104,二進制是1101000:


"e"的ascii碼是65,二進制是101,二進制是1100101:


然後形成如下的位圖:


下面就需要利用位操作來進行設置:

test:1>setbit s 1 1
"0"
test:1>setbit s 2 1
"0"
test:1>setbit s 4 1
"0"
test:1>setbit s 10 1
"0"
test:1>setbit s 13 1
"0"
test:1>setbit s 9 1
"0"
test:1>setbit s 15 1
"0"

setbit的順序可以隨意調整,只要最終得到的位圖是如上形式的就OK了。(我這裏就調整了下seitbit的順序,好吧,我承認其實我是打錯了,又懶得再去打一遍,反正最終形成的位圖是一樣的)。

然後我們get一下:

test:1>get s
"he"

很神奇,有木有,這也說明了在Redis的底層,String就是一個數組,而且還是一個byte[]。

SDS

不管在什麼編程語言、存儲引擎中,String都是應用最廣泛的,而在不同的編程語言、存儲引擎中,String可能有不同的實現,在Redis中,String的底層就是SDS,它的全稱是Simple Dynamic String。

Redis是C語言開發的,Redis爲什麼不直接利用C語言的字符串,而要“別出心裁”的自己構建SDS數據結構來實現字符串呢?

我們先來這個SDS是個什麼鬼:

struct sdshdr {
    int len;
    int free;
    char buf[];
};

SDS的定義比較簡單,只有3個字段,而且從字面上就可以看出是什麼意思:

  • len:存儲字符串的實際長度
  • free:存儲剩餘(空閒)的空間
  • buf[]:存儲實際數據

下面我們就來看下SDS和C語言的字符串有什麼區別:

  • 求字符串長度
    在C語言中,求字符串的長度只能遍歷,時間複雜度是o(n),單線程的Redis表示鴨梨山大,但是現在引入了一個字段來存儲字符串的實際長度,時間複雜度瞬間降低成了o(1)。
  • 二進制安全
    在C語言中,讀取字符串遵循的是“遇零則止”,即,讀取字符串,當讀取到“\0”,就認爲已經讀到了結尾,哪怕後面還有字符串也不會讀取了,像圖片、音頻等二進制數據,經常會穿插“\0”在其中,好端端的圖片、音頻就毀了...但是現在有了一個字段來存儲字符串的實際長度,讀取字符串的時候,先看下這個字符串的長度是多少,然後往後讀多少位就可以了。
  • 緩衝區溢出
    字符串拼接是開發中常見的操作,C語言的字符串是不記錄字符串長度的,一旦我們調用了拼接函數,而沒有提前計算好內存,就會產生緩衝區溢出的情況,但是現在引入了free字段,來記錄剩餘的空間,做拼接操作之前,先去看下還有多少剩餘空間,如果夠,那就放心的做拼接操作,不夠,就進行擴容。
  • 減少內存重分配次數
  1. 空間預分配:當對字符串進行拼接操作的時候,Redis會很貼心的分配一定的剩餘空間,這塊剩餘空間現在看起來是有點浪費,但是我們如果繼續拼接,這塊剩餘空間的作用就出來了。
  2. 惰性空間釋放:當我們做了字符串縮減的操作,Redis並不會馬上回收空間,因爲你可能即將又要做字符串的拼接操作,如果你再次操作,還是沒有用到這部分空間,Redis也會去回收這部分空間。

擴容策略

字符串小於1M,採用的是加倍擴容的策略,也就是多分配100%的剩餘空間,當大於1M,每次擴容,只會多分配1M的剩餘空間。

最大長度

Redis 規定字符串的長度不得超過 512M 字節。

embstr raw

Redis的字符串有兩種存儲方式,一種是embstr,一種是raw,當長度<=44,採用embstr 來存儲:

set codebear abcdefghijklmnopqrstuvwxyz012345678912345678
"OK"
debug object codebear
"Value at:0x7f4050476880 refcount:1 encoding:embstr serializedlength:45 lru:1999016 lru_seconds_idle:36"

當長度>44,改用raw來存儲:

set codebear abcdefghijklmnopqrstuvwxyz0123456789123456781
"OK"
debug object codebear
"Value at:0x7f404ac30100 refcount:1 encoding:raw serializedlength:46 lru:1999188 lru_seconds_idle:3"

網上也有一些博客說是以39爲分界線,爲什麼會有兩種答案呢?繼續看下去就明白了。

我們先來看看Redis的對象頭,查看

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

Redis對象頭佔 4bit+4bit+24bit+4byte+8byte(指針,在64 bit system下,佔8byte)=32bit+12byte=4byte+12byte=16byte。

再來看看這兩種存儲形式有什麼區別:



embstr的存儲形式比較緊湊,Redis的對象頭和SDS對象存在一起(連續)。


一般來說,在raw的存儲形式下,Redis的對象頭和SDS對象不存在一起(不連續)。

我們可以簡單的理解爲,一塊內存的大小爲64byte。

好了,前置內容介紹完畢了,我們來看看Redis3.0版本的SDS的定義,查看

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

Redis對象頭佔了16byte,SDS對象的len和free又佔了8byte,64-16-8=40,同時保存的字符串會以\0結尾,又佔用了1byte,所以實際存儲的字符串只能<=39位,所以在低版本的Redis下,embstr、raw的分界線爲39。

再來看看Redis5.0版本的SDS的定義,查看


可以看到變化很大,爲什麼要做那麼大的改變?更節省內存,當字符串長度比較小的時候,會用
sdshdr8來存儲,len和alloc共佔用2byte,flags佔用1byte,\0結尾佔用1byte,一共是4byte,64byte-16byte(對象頭)-4byte=44byte,所以在高版本的Redis下,embstr、raw的分界線爲44。

怎麼樣,沒想到吧,我們Redis經常使用的String竟然牽扯到那麼多東西,而這些東西就可以區分平庸開發和優秀開發,成爲一個優秀的開發,要學習的東西還有很多很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章