當(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ì)說了族展,小伙伴們有個大概了解就好森缠。
- 內(nèi)存釋放的時候embstr編碼的字符串只需要釋放一次內(nèi)存,而raw類型需要釋放兩次內(nèi)存仪缸。
- 因為embstr這種編碼的字符串?dāng)?shù)據(jù)是存放在連續(xù)的一塊內(nèi)存里面贵涵,和raw編碼的字符串相比效率更高。
下面用三張圖來分別表示這三種編碼格式:
四. 實踐
說了一堆理論恰画,對于這三種編碼分別在什么場景下使用小伙伴們可能還是不夠了解宾茂,下面來舉幾個例子:
需要說明的是筆者這里沒有設(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é)果:
那下面我們再看下createStringObjectFromLongLongForValue這個函數(shù)里面的執(zhí)行步驟:
從調(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í)行的過程:
這里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
雖然字符串是整數(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ù)吴汪。