Redis 字符串對象及其編碼詳解

當(dāng)我們在redis里面保存一個鍵值對的時候钦铁,我們至少會創(chuàng)建兩個對象羡滑,一個對象用作鍵值對的鍵(鍵對象)瞳腌,另外一個對象用作鍵值對的值(值對象),下面先來介紹下redis 對象的結(jié)構(gòu)很泊,然后再來看下字符串對象。筆者的redis版本是5.0.7

一. Redis 對象定義(server.h)

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;

這是源碼中關(guān)于redis對象的定義沾谓,下面就每個字段含義做個簡單的介紹委造。
1) type : 表示對象的類型,分別有字符串對象均驶,列表對象昏兆,哈希對象,集合對象妇穴,有序集合對象爬虱,利用這個字段redis可以在命令執(zhí)行之前來判斷一個對象是否可以執(zhí)行給定的命令。
2)encoding: 數(shù)據(jù)編碼方式腾它,總共有8種分別是(不同的版本略有不同):
a . OBJ_ENCODING_INT // long類型的整數(shù)
b. OBJ_ENCODING_EMBSTR // embstr編碼的簡單動態(tài)字符串
c. OBJ_ENCODING_RAW //簡單動態(tài)字符串
d. OBJ_ENCODING_HT //字典
e. OBJ_ENCODING_QUICKLIST //雙端列表
f. OBJ_ENCODING_ZIPLIST // 壓縮列表
g. OBJ_ENCODING_INTSET //整數(shù)集合
h. OBJ_ENCODING_SKIPLIST //跳躍表

3) lru: Least Recently Used即最近最少使用跑筝,LFU(最不頻繁使用的)也可以使用這個字段,LRU和LFU是兩種不同的算法瞒滴,它們的主要作用是當(dāng)redis內(nèi)存不足時淘汰那些不常使用的key曲梗。當(dāng)然這需要配置,默認(rèn)是不限制使用的內(nèi)存的,也沒有設(shè)定淘汰算法稀并,一般情況下我們會配置同時配置maxmemory和maxmemory-policy兩個參數(shù)仅颇。雖然默認(rèn)是不限制使用的內(nèi)存大小的,但是并不意味著程序可以無限制的使用內(nèi)存碘举,如果你的操作系統(tǒng)同時在運行多個程序忘瓦,其中某個程序占用了全部的內(nèi)存,那就會導(dǎo)致其他程序無法運行引颈。

4)refcount: 引用計數(shù)耕皮,用來實現(xiàn)對象共享,多個key 指向同一個值對象蝙场,從而可以節(jié)約內(nèi)存凌停。
5) ptr : 無類型指針,指向真正的數(shù)據(jù)售滤。對于不同的數(shù)據(jù)類型罚拟,redis會以不同的形式來存儲。

二. 字符串對象

通常情況下我們通過set key value 就可以設(shè)置一個字符串對象(當(dāng)然還有其他的命令)完箩,例如:

redis > set hello world
OK
redis > get hello
"world"
redis > set number 10
OK
redis > get number
"10"
redis > type hello
"string"
redis > type number 
"string"

上面設(shè)置了兩個key赐俗,通過type命令可知它們都是字符串對象,不過需要注意的是鍵number雖然是整數(shù)弊知,redis也會將其轉(zhuǎn)換為字符串來存儲阻逮。

三. 字符串的三種底層編碼

redis > object encoding hello
"embstr"
redis > object encoding number
"int"
redis > set msg "這是一條消息,這是一條消息秩彤,這是一條消息叔扼,這是一條消息"
OK
redis > object encoding msg
"raw"

上面的例子里面包含了字符串對象所使用的全部編碼類型,分別是:int漫雷,embstr瓜富,raw。下面來分別介紹下:
1. INT 編碼
如果一個字符串對象保存的是整數(shù)值珊拼,并且這個整數(shù)值可以用long類型來表示食呻,那么字符串對象會將整數(shù)值保存在字符串對象結(jié)構(gòu)的ptr屬性里面(將void* 轉(zhuǎn)換成long),并將字符串對象的編碼設(shè)置為int。
下面摘取源碼中的部分代碼(object.c文件)幫助大家來理解:

//轉(zhuǎn)碼函數(shù)澎现,判斷對象是否能被整數(shù)編碼仅胞,否則不做處理
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
if (!sdsEncodedObject(o)) return o;
 if (o->refcount > 1) return o;

len = sdslen(s);
if (len <= 20 && string2l(s,len,&value)) {
   if ((server.maxmemory == 0 ||
        !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
        value >= 0 &&
        value < OBJ_SHARED_INTEGERS)
    {
        decrRefCount(o);
        incrRefCount(shared.integers[value]);
        return shared.integers[value];
    } else {
        if (o->encoding == OBJ_ENCODING_RAW) {
            sdsfree(o->ptr);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
            decrRefCount(o);
            return createStringObjectFromLongLongForValue(value);
        }
    }
}

robj *createStringObjectFromLongLong(long long value) {
    return createStringObjectFromLongLongWithOptions(value,0);
}

// long 類型的字符串對象
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj) {
    robj *o;

    if (server.maxmemory == 0 ||
        !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS))
    {
        /* If the maxmemory policy permits, we can still return shared integers
         * even if valueobj is true. */
        valueobj = 0;
    }

    if (value >= 0 && value < OBJ_SHARED_INTEGERS && valueobj == 0) {
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    } else {
        if (value >= LONG_MIN && value <= LONG_MAX) {
            o = createObject(OBJ_STRING, NULL);
            o->encoding = OBJ_ENCODING_INT;
            o->ptr = (void*)((long)value);
        } else {
            o = createObject(OBJ_STRING,sdsfromlonglong(value));
        }
    }
    return o;
}

這個tryObjectEncoding方法就是redis里面對字符串對象內(nèi)部轉(zhuǎn)碼的方法,以此來達(dá)到節(jié)約內(nèi)存的目的剑辫。小伙伴對于 len< 20 可能會有點疑惑干旧,因為有符號的long類型的取值范圍是 -2^63 - 2^63-1 這個數(shù)字的最大長度恰好是19位。
另外妹蔽,當(dāng)實例沒有設(shè)置maxmemory限制或者maxmemory-policy設(shè)置了淘汰算法的時候椎眯,如果設(shè)置的字符串鍵在0-10000內(nèi)的數(shù)字挠将,則可以直接引用共享對象而不用再建立一個redisObject。注: Redis在啟動后會預(yù)先建立10000個分別存儲從0到9999這些數(shù)字的redisObject類型變量作為共享對象编整。

2. embstr編碼
如果字符串對象保存的是字符串值舔稀,并且這個字符串的長度小于等于44個字節(jié)(一些老一點的版本是32個字節(jié)),那么字符串對象將使用embstr編碼的方式來保存這個字符串值掌测;如果大于44個字節(jié)將使用raw編碼内贮。下面貼一段源碼片段:

//創(chuàng)建string對象
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

3. raw 編碼
如果字符串對象保存的是字符串值,并且這個字符串的長度大于44個字節(jié)汞斧,那么字符串對象將使用raw編碼的方式來保存這個字符串值夜郁,有小伙伴可能會比較疑惑為啥是44個字節(jié),因為jemalloc內(nèi)存分配器每次分配的內(nèi)存大小都是2的整數(shù)倍粘勒,至少分配32個字節(jié)的內(nèi)存竞端,大一點就是64個字節(jié),再大一點將使用raw 編碼庙睡,由于redisObject的大小是16個字節(jié)事富,再加上sdshdr 的3個字節(jié)和一個字符結(jié)尾的\0, 即 64-16-3-1 = 44;

#define LRU_BITS 24
....
typedef struct redisObject {
    unsigned type:4;       //4bit
    unsigned encoding:4;   //4bit
    unsigned lru:LRU_BITS; //24bit
    int refcount;          //4byte   int 是4個字節(jié)
    void *ptr;             //8byte     可以通過C代碼驗證
} robj;

//從源碼可以看出sdshdr5實際是不會使用的埃撵,而能使用的最小sdshdr 就是sdshdr8了
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 1個字節(jié)
    uint8_t alloc; //1個字節(jié)
    unsigned char flags; //1個字節(jié)
    char buf[];      // 包含一個\0 結(jié)束符占1個字節(jié)
};

#include <stdio.h>
#include <stdlib.h>
struct robj {
    void *ptr;
 };
int main () {
     struct robj t;
     printf("大小:%d",sizeof(t));
}    

輸出是8個字節(jié)
那embstr 和raw 有什么區(qū)別呢赵颅?
1.首先它們都是使用redisObject結(jié)構(gòu)和sdsstr結(jié)構(gòu)來表示字符串對象虽另,但raw編碼會調(diào)用兩次內(nèi)存分配函數(shù)來分別創(chuàng)建redisObject結(jié)構(gòu)和sdshdr結(jié)構(gòu)暂刘,而embstr編碼則通過調(diào)用一次內(nèi)存分配函數(shù)來分配一塊連續(xù)的空間,分別包含redisObject和sdshdr兩個結(jié)構(gòu)捂刺。
下面貼下sds數(shù)據(jù)結(jié)構(gòu)

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

這里是源碼中關(guān)于sds的定義谣拣,對于不同長度的字符串會采用不同的sds來存儲,這里就不詳細(xì)說了族展,小伙伴們有個大概了解就好森缠。

  1. 內(nèi)存釋放的時候embstr編碼的字符串只需要釋放一次內(nèi)存,而raw類型需要釋放兩次內(nèi)存仪缸。
  2. 因為embstr這種編碼的字符串?dāng)?shù)據(jù)是存放在連續(xù)的一塊內(nèi)存里面贵涵,和raw編碼的字符串相比效率更高。

下面用三張圖來分別表示這三種編碼格式:


INT編碼示意圖.png

RAW編碼.png

embstr.png

四. 實踐

說了一堆理論恰画,對于這三種編碼分別在什么場景下使用小伙伴們可能還是不夠了解宾茂,下面來舉幾個例子:
需要說明的是筆者這里沒有設(shè)置maxmemory,也就是maxmemory=0
例1:

redis > set num 120
OK
redis > object encoding num
"int"
redis > type num
string
redis > strlen num
(integer) 3

這里我們設(shè)置了一個字符串類型的對象,編碼為int拴还,因為120在0~10000之內(nèi)跨晴,所以這里redis進(jìn)行對象轉(zhuǎn)碼使用共享對象,不需要再次創(chuàng)建redisObject片林。
例2:

redis > set num1 10001
OK
redis > object encoding num1
"int"
redis > type num1
string
redis> strlen num1
(integer) 5

這里我們同樣設(shè)置了一個字符串類型的對象端盆,編碼為int怀骤,只不過大于10000的數(shù)據(jù)。下面展示下gdb工具單步調(diào)試的結(jié)果:


gdb.png

那下面我們再看下createStringObjectFromLongLongForValue這個函數(shù)里面的執(zhí)行步驟:


gdb2.png

從調(diào)試的結(jié)果看redis對對象進(jìn)行了一次轉(zhuǎn)碼焕妙,由于值超出了共享對象的范圍蒋伦,但是在long類型的范圍之內(nèi),所以仍然可以使用int編碼焚鹊。

例3:

redis > set str 'hello'
OK
redis > object encoding str
"embstr"
redis > type str
string
redis > strlen str
(integer) 5

這里我們設(shè)置了embstr編碼的字符串(小于等于44個字節(jié)使用embstr)凉敲,同樣使用gdb工具來分析下執(zhí)行的過程:


gdb3.png

這里redis沒有對其進(jìn)行轉(zhuǎn)碼,因為一方面該字符串不是long類型能表示的字符串寺旺,另外由于字符串的長度小于44個字節(jié)爷抓,并且字符串原來的編碼就是embstr,所以這里不做處理阻塑。
例4:

redis > set str 111111111111111111111111111111111111111111111
OK
redis > object encoding str
"raw"
redis > type str
string
redis > strlen str
(integer) 45
gdb4.png

雖然字符串是整數(shù)類型的蓝撇,但是超出long范圍,另外長度也大于44個字節(jié)陈莽,這里就沒有做轉(zhuǎn)碼操作渤昌,而是直接返回。

總結(jié):

本文向大家展示了redis字符串對象以及它的三種編碼方式走搁,int独柑,embstr,raw私植,
1) redis在創(chuàng)建字符串的時候會首先根據(jù)字符串的長度來判斷是創(chuàng)建embstr編碼(長度小于等于44字節(jié))的對象還是raw編碼的對象忌栅。
2) redis內(nèi)部轉(zhuǎn)碼(只會對raw和embstr兩種格式進(jìn)行轉(zhuǎn)碼),redis會使用tryObjectEncoding函數(shù)優(yōu)化對象的編碼方式 曲稼,主要是看對象是否能被整數(shù)編碼索绪,否則不做處理。能被整數(shù)編碼大致有三種情況:
前提是能被long類型表示的整數(shù)型字符串
a.當(dāng)實例沒有設(shè)置maxmemory限制或者maxmemory-policy設(shè)置了淘汰算法的時候贫悄,且value>0 && value <=10000的時候使用的是共享對象瑞驱,這些共享對象的編碼是int
b. 在不滿足a的情況下,且當(dāng)前對象的編碼為raw編碼的時候會設(shè)置為int窄坦,參考源代碼:


源代碼1.png

c. 在不滿足a的情況下唤反,且當(dāng)前對象的編碼為raw編碼的時候會設(shè)置為int,參考源代碼:


源代碼2.png

3) 內(nèi)部轉(zhuǎn)碼發(fā)生的時候,在使用set命令鸭津,append等命令的時候都可能會發(fā)生內(nèi)部轉(zhuǎn)碼彤侍,比如通過一些命令使得原來的字符串發(fā)生了改變,如果原來是raw編碼的后來字符串的長度縮小了可以使用embstr來編碼曙博,那這個時候就會發(fā)生轉(zhuǎn)碼拥刻。

先寫到這里,由于redis的源碼本人也沒有全部看完父泳,如果有不對的地方歡迎各位指出般哼,看到會及時回復(fù)吴汪。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蒸眠,隨后出現(xiàn)的幾起案子漾橙,更是在濱河造成了極大的恐慌,老刑警劉巖楞卡,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霜运,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒋腮,警方通過查閱死者的電腦和手機淘捡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來池摧,“玉大人焦除,你說我怎么就攤上這事”炱牵” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵竭讳,是天一觀的道長创葡。 經(jīng)常有香客問我,道長绢慢,這世上最難降的妖魔是什么灿渴? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮呐芥,結(jié)果婚禮上逻杖,老公的妹妹穿的比我還像新娘。我一直安慰自己思瘟,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布闻伶。 她就那樣靜靜地躺著滨攻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蓝翰。 梳的紋絲不亂的頭發(fā)上光绕,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音畜份,去河邊找鬼诞帐。 笑死,一個胖子當(dāng)著我的面吹牛爆雹,可吹牛的內(nèi)容都是我干的停蕉。 我是一名探鬼主播愕鼓,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼慧起!你這毒婦竟也來了菇晃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蚓挤,失蹤者是張志新(化名)和其女友劉穎磺送,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灿意,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡估灿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了缤剧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甲捏。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鞭执,靈堂內(nèi)的尸體忽然破棺而出司顿,到底是詐尸還是另有隱情,我是刑警寧澤兄纺,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布大溜,位于F島的核電站,受9級特大地震影響估脆,放射性物質(zhì)發(fā)生泄漏钦奋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一疙赠、第九天 我趴在偏房一處隱蔽的房頂上張望付材。 院中可真熱鬧,春花似錦圃阳、人聲如沸厌衔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽富寿。三九已至,卻和暖如春锣夹,著一層夾襖步出監(jiān)牢的瞬間页徐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工银萍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留变勇,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓贴唇,卻偏偏與公主長得像搀绣,于是被迫代替她去往敵國和親飞袋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載:可能是目前最詳細(xì)的Redis內(nèi)存模型及應(yīng)用解讀 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一豌熄,通過在內(nèi)存中讀寫數(shù)據(jù)...
    jwnba24閱讀 623評論 0 4
  • 前言 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一授嘀,通過在內(nèi)存中讀寫數(shù)據(jù),大大提高了讀寫速度锣险,可以說Redis是實現(xiàn)網(wǎng)站...
    小陳阿飛閱讀 805評論 0 1
  • 想記錄一下自己這次失戀的心路歷程蹄皱,感覺還挺有意思的。 第一芯肤,痛苦的逃避了一個星期巷折。同時抱有幻想還會被挽回,所以糾結(jié)...
    干鍋小魚煲閱讀 134評論 2 0
  • Session3的思考題:你的產(chǎn)品滿足了顧客什么需求崖咨,有哪些直接和潛在競爭對手锻拘? 1. 從產(chǎn)品直接功效的角度,提供...
    孫藝嘉閱讀 499評論 0 0