Redis 之簡單動態字符串

在開始今天主題之前,先說說 C 中的字符串,做個鋪墊。

在 C 中是沒有字符串這一類型的,替代的方案是用字符指針和字符數組來實現,同時 C 中是用長度 N+1 來存放字符串,N 就是字符串的長度, 1 爲最後的空字符 NUL,在 ASCII 碼中爲 \0。

char *buf = "molaifeng"; /* 會在末尾位置放置一個 \0 */ 

buf 存儲 molaifeng 所在內存的首地址,同時注意 char *buf 是存在常量區的,不能被改寫的。

*buf = 'M'
printf("%d", buf);

運行後,會報段錯誤(Segmentation fault)。

char buf[] =  {'m', 'o', 'l', 'a', 'i', 'f', 'e', 'n', 'g', '\0'};
char buf[] =  {"molaifeng"}; /* 會在末尾位置放置一個 \0 */
char buf[] =  "molaifeng"; /* 會在末尾位置放置一個 \0 */

上面三種方式的字符數組都能實現字符串的定義,char buf[] 是存儲在棧區的,是可以改寫的。

*buf = 'M';
printf("%s\n", buf);

運行輸出 Molaifeng,第一個字符被改成大寫的了。

在 C 中獲取字符串的長度,需要遍歷字符串中的每個字符,直至遇到 \0 爲止,是線性級別的,字符串越長,遍歷耗費的時間越久,複雜度爲 O(N)。同時,由於規定了 \0 爲終止符,那麼註定就不能存儲一些中間有 \0 的數據,比如圖片、音視頻之類二進制數據,也就是說是非二進制安全的。

由於 C 中字符串操作的需求是很頻繁的,於是便有了 string.h 標準庫,裏面申明定義了常用的字符串操作函數,都是些短小精煉的且經過時間、業務檢驗的函數,值得一看。

簡要介紹了 C 的字符串,再來談談 Redis 中廣泛使用的數據結構 SDS,也被稱爲簡單動態字符串。在看 Redis 各版本特性時,發現 SDS 結構是在 3.2 版本上做了速度和節省空間上的優化。

下面是 3.2 版本之前的結構。

// sds.h

struct sdshdr {
	int len; /* buf 數組已佔用的字節數 */
	int free; /* buf 數組中可用的字節數 */
	char buf[]; /* 數組空間 */
};

整個結構體佔 8 個字節,爲啥沒有把 buf 數組算進去,這是因爲 char buf[] 利用了 C 的 flexible array 特性,中文叫柔性數組,定義時放在結構體最後,不申明空間,不佔結構體空間,其內存空間緊鄰結構體。

len 佔 4 個字節,最多可存儲 2^64 = 512M 長度的字符串,由於指明瞭長度,那麼 Redis 獲取 SDS 字符串長度效率就是 O(1) 了,這比 C 中通過遍歷直到遇到空字符 \0 爲止而獲取字符串長度的複雜度 O(N) 不知快多少,這也是典型的空間換時間的場景。

free 佔 4 個字節,用來存儲 buf 數組中可用的字節數,這個字段主要用來實現空間預分配和惰性釋放的。

127.0.0.1:6379> set name molaifeng
OK

執行完此命令後的內存佈局如下。
在這裏插入圖片描述
len 爲 9, free 爲 0,buf 字符數組存儲了實際的值,注意,在結尾處加了 ‘\0’,這個是 Redis 自己加的,是爲了兼容 C 的字符串,比如打印上面存儲的字符串,就可以直接使用 C 中標準的字符串處理庫中的 printf 函數,而不用重複造輪子。

prinf("%s", s->buf);

再來更新下 name 的值。

127.0.0.1:6379> append name " is here"
(integer) 25
127.0.0.1:6379> get name
"molaifeng is here"

此時的內存佈局如下。
在這裏插入圖片描述
len 爲 17,代表更新後的字符串長度;free 爲 17,代表目前還未使用的字符串長度,但這個 17 是如何計算的,看看下面的這個代碼。

// sds.h

#define SDS_MAX_PREALLOC (1024*1024)

// sds.c

newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
else
    newlen += SDS_MAX_PREALLOC;

原來,在更新後的字符串值長度小於 1M 時,那麼此是 buf 數組的長度爲 2 倍的新的字符串的長度再加 1 也就是 17*2 + 1 = 35 了(那 1 個字節長度爲 \0 佔用的空間),len 存儲了已使用的長度,那麼 free 也就是未使用的長度,也就是 17 了;如果新的字符串長度大於 1M,那麼 SDS 會分配 1M 的未使用的空間。再下次再追加字符串時,如果 free 的空間夠用,就不用再重新申請內存,這種策略就是預分配了。

如果此時對 name 裏的值進行縮短操作,那麼此時 Redis 並不會立馬把多出來的字節回收,而是使用 free 把多餘的字節數量加起來以便將來預分配使用。這就是惰性釋放策略,可以避免頻繁的內存重分配,當然了,Redis 裏也提供了真正釋放 SDS 內存的 API,真正釋放未使用的內存空間。

sdshdr 結構雖然精巧,但是還是有優化的空間。比如,int 類型 len 佔 4 個字節,最多可存多達 512M 的字符串,可上面一開始存的 molaifeng 這 9 個長度的字符串,實際上 1 個字節就表示其長度了(1 個字節可以存儲 2^8 = 256 個長度的字符串),剩餘的 3 個字節就浪費了,free 也是同理。一旦生產中 Redis 存儲的類似的短字符串量大的話,造成的內存浪費也是很可觀的。鑑於此,Redis 3.2 便優化了 sdshdr 結構,依據 1、2、4、8 個字節分別定義了 sdshdr8、sdshdr16、sdshdr32、sdshdr64 四個結構,其實還有個 sdshdr5,實際上沒用到,符合此結構的都提升到了 sdshdr8 了。

// sds.h

/* 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[];
};

新的結構中,len 指已經使用的長度;alloc 是申請的內存大小;flags 低三位用於表示 sds 類型,在 Redis 中有五種,分別用宏定義了。

// sds.h

#define SDS_TYPE_5  0 /* 00000 000 */
#define SDS_TYPE_8  1 /* 00000 001 */
#define SDS_TYPE_16 2 /* 00000 010 */
#define SDS_TYPE_32 3 /* 00000 011 */
#define SDS_TYPE_64 4 /* 00000 100 */

__attribute__ ((__packed__)) 這個主要告訴編譯器,這個結構體是以 1 個字節對齊。

再看介紹下一個很有用的宏。

// sds.h

typedef char *sds;

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

這個宏的作用是獲取 sdshdr 結構體的的起始指針,比如傳了 SDS_HDR(8,s),那麼先計算結構體 sdshdr8 的佔用的內存空間(sdshdr##T 中 ## 在 C 的宏中是個連接符號,T 爲 8,那麼就是 sdshdr8),然後 s 指針偏移 sdshdr8 大小的內存,從而獲取起始指針。幹說還是有點枯燥,再來張圖。

在這裏插入圖片描述
再來看看幾個常用的函數。

// sds.h

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

sdsReqType 依據字符串的長度,獲取 sdshdr 的類型,分 5 段,返回 5 個類型。

// sds.h

#define SDS_TYPE_MASK 7

static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

sdsHdrSize 獲取 sdshdr 佔用的內存空間,主要就看 type&SDS_TYPE_MASK 這個與運算。依據 sdsReqType 獲取的類型,比如爲 SDS_TYPE_8,那麼就是 1&7 = 00000001 & 00000111 = 00000001 = 1,走 case SDS_TYPE_8 這裏,然後返回 sdshdr8 佔的內存大小。

// sds.h

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

sdslen 獲取對應類型中 buf 數組存放的字符串長度。這裏比較繞的就是 s[-1],因爲 s 指向的是柔性數組,而柔性數組和其結構體是緊挨着的,那麼偏移 -1 就是指向了 flags 字段了,前面說過此字段存放的是 sdshdr 的類型,這就是和 flags = s[-1] 對上了。flags&SDS_TYPE_MASK 也已經介紹,依據獲取的類型不同,走到不同的 case 裏,如果爲 SDS_TYPE_8,那麼通過 SDS_HDR 獲取 sdshdr8 的起始指針,再返回其 len 值。也可以結合下圖理解下。
在這裏插入圖片描述

// sds.h

static inline size_t sdsavail(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
            return 0;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            return sh->alloc - sh->len;
        }
    }
    return 0;
}

sdsavail 看看當前 sdshdr 容量是否夠用,有了前面的鋪墊,就一目瞭然,就是申請的內存減去已使用的內存。

// sds.h

static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}

sdssetlen 設置 sds 新使用長度,就是把 newlen 賦值給 s 的 len,費勁吧啦的計算 SDS_TYPE_5,可是在 Redis 5 版本中卻不用,應該是兼容吧。這裏還是說下 sdshr5 吧,flags 字段的低 3 位爲類型,高 5 位爲長度,((unsigned char*)s)-1 獲取 flags 字段,newlen << SDS_TYPE_BITS 左移 3 位,也就是把新的長度存儲到高 5 位那,然和 SDS_TYPE_5 做或運算,這裏沒發現或運算有啥用,因爲 SDS_TYPE_5 是 0,也就是二進制位都是 0,不管和誰做或運算,都等於對方。

其他相關的輔助函數就不多說介紹了,可以看看源碼,這裏再說下 SDS 創建和釋放。

// sds.h

/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

sdsnew 創建一個新的 sds 的字符串,新獲取字符串的長度,再調用 sdsnewlen 函數

/* Create a new sds string with the content specified by the 'init' pointer
 * and 'initlen'.
 * If NULL is used for 'init' the string is initialized with zero bytes.
 * If SDS_NOINIT is used, the buffer is left uninitialized;
 *
 * The string is always null-termined (all the sds strings are, always) so
 * even if you create an sds string with:
 *
 * mystring = sdsnewlen("abc",3);
 *
 * You can print the string with printf() as there is an implicit \0 at the
 * end of the string. However the string is binary safe and can contain
 * \0 characters in the middle, as the length is stored in the sds header. */
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen); /* 依據長度獲取 sdshdr 類型 */
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; /* 如何是 sdshr5 則提升至 sdshdr8 */
    int hdrlen = sdsHdrSize(type); /* 獲取 sdshdr 類型的內存大小,用來申請內存用 */
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1); /* 申請內存,hdrlen 爲頭部空間,initlen 爲字符串長度,1 爲 \0 長度  */
    if (init==SDS_NOINIT) /* 如果指定初始內容爲 SDS_NOINIT,則把 init 置爲 NULL */
        init = NULL;
    else if (!init) /* 如果沒有指定初始內容,則把 sh 內存清 0 */
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL; /* 如果爲空指針返回 NULL */
    s = (char*)sh+hdrlen; /* s 指向柔性數組 */
    fp = ((unsigned char*)s)-1; /* fp 爲類型指針 */
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen); /* 從 init 複製 initlen 個字符到存儲區 s */
    s[initlen] = '\0'; /* 末尾添加 \0 結束符 */
    return s;
}

有創建就有釋放了。

// sds.c

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
    if (s == NULL) return;
    /*
	 * s[-1] 偏移到 flags 獲取類型
	 * sdsHdrSize 獲取具體類型所佔的內存空間
	 * (char *)s-sdsHdrSize 偏移到具體類型的起始指針
	 */
    s_free((char*)s-sdsHdrSize(s[-1]));
}

最後來講下 SDS 的編碼。在前面 淺談 Redis 中介紹過字符串的三中編碼 int、embstr 和 raw,其中 int 是直接把值存儲到 ptr 裏; embstr 則是在申請內存時和 redisObject 緊鄰着,不用申請兩次,類似於柔性數組,一起申請了;raw 則需要申請兩次,redisObject 和 sdshdr。下面就來詳細介紹下具體實現,達到知其然並知其所以然。

// 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;

如果是跟着這個系列的看客應該認識這個結構,在 Redis 中 key 和 value 外面都包了層這個統一的 Redis 對象,這裏僅說與主題相關的字符串。

// server.h

#define OBJ_STRING 0    /* String object. */

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

type 爲 0 時,說明是字符串類型的,encoding 中 0、1、8 分別對應字符串的 raw、int、embstr 編碼。當客戶端發送字符串的相關寫入、修改命令時,服務端選擇哪種類型的編碼,這裏就深入源碼,熟悉其本質,從而達到豁然開朗境界。

// object.c

/* Create a string object with EMBSTR encoding if it is smaller than
 * OBJ_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
 * used.
 *
 * The current limit of 44 is chosen so that the biggest string object
 * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#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);
}

createStringObject 函數通過判斷字符串的長度來決定底層使用什麼編碼來存儲。乍一看,爲什麼是 44 這個值,是不是什麼魔數?我們先來看看如果條件成立的下,程序進入到 createEmbeddedStringObject,會設置字符串的 encoding 爲 embstr,使用 sdshdr8 結構體來存儲字符串,在 Redis 中 embstr 是一塊連續的內存區域 ,redisObject 和 sdshdr8 毗鄰着,所以就應了上面的 embstr 只需分配一次內存的結論。這裏再回顧下之前 sdshdr8 結構體佔多少內存,1 個字節的 len 加上 1 個字節的 alloc 再加上 1 個字節的 flags 等於 3 個字節,再加上緊鄰的 buf 數組的 44 以及末尾的 \0 一個字節是多少字節呢,1+1+1+44+1 = 48 個字節,再加上毗鄰的的 redisObject 內存 16 個字節,那麼就是 64 個字節了。Redis 默認採用 jemalloc 來管理內存,會分配 8、16、32、64 等字節的內存,embstr 正好處在 64 字節這個檔位,方便分配和回收。

// object.c

/* Create a string object with encoding OBJ_ENCODING_EMBSTR, that is
 * an object where the sds string is actually an unmodifiable string
 * allocated in the same chunk as the object itself. */
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1); /* 這裏也可以看出,embstr 內存是一次分配的 */
    struct sdshdr8 *sh = (void*)(o+1); /* 這裏 redisObject 指針向前移動一位,就是 sdshr8 的指針了,和之前 s[-1] 是一樣的作用 */

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1; /* sh 指向 sdshdr8,+1 後就指向柔性數組 buf 了 */
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

在這裏插入圖片描述

// object.c

/* Create a string object with encoding OBJ_ENCODING_RAW, that is a plain
 * string object where o->ptr points to a proper sds string. */
robj *createRawStringObject(const char *ptr, size_t len) {
    return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}

創建 raw 類型的字符串分配了兩次內存,先是前面介紹的 sdsnewlen,依據 len 選擇合適的 sdshdr 分配內存,再就是分配關聯的 redisObject 的內存。

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o)); /* 給 robj 分配空間  */
    o->type = type; /* 指定什麼類型,此篇博文下就是 0 */
    o->encoding = OBJ_ENCODING_RAW; /* 默認爲 OBJ_ENCODING_RAW 編碼 */
    o->ptr = ptr; /* o->ptr 指向 sdshdr 具體類型的 buf 指針 */
    o->refcount = 1; /* 計數爲 1 */

    /* Set the LRU to the current lruclock (minutes resolution), or
     * alternatively the LFU counter. 
     *  設置 lru 策略
     * */
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;  /* 返回 robj 類型的指針 */
}

在這裏插入圖片描述
看了上面的代碼及解釋,發現 embstr 的優勢很大,不過翻了翻源碼發現,這個是隻讀的,也就是第一次創建後,如果下次把值修改了,即使長度沒超過 44,還是會轉爲 raw 的。

127.0.0.1:6379> set name molaifeng
OK
127.0.0.1:6379> object encoding name
"embstr"
127.0.0.1:6379> append name " is here"
(integer) 17
127.0.0.1:6379> object encoding name
"raw"

在編碼轉換時,又會進行兩次內存分配,如果頻繁的話,對性能是會有損耗的,所以得提前規劃好,對於 key 來說,儘量把長度控制在 44 以內,對於 value 就看實際業務了。

127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> object encoding age
"int"

當字符串對象裏保存的是整數時,其編碼爲 int,下面來看看具體實現

// object.c

/* Wrapper for createStringObjectFromLongLongWithOptions() avoiding a shared
 * object when LFU/LRU info are needed, that is, when the object is used
 * as a value in the key space, and Redis is configured to evict based on
 * LFU/LRU. */
robj *createStringObjectFromLongLongForValue(long long value) {
    return createStringObjectFromLongLongWithOptions(value,1);
}

/* Create a string object from a long long value. When possible returns a
 * shared integer object, or at least an integer encoded one.
 *
 * If valueobj is non zero, the function avoids returning a a shared
 * integer, because the object is going to be used as value in the Redis key
 * space (for instance when the INCR command is used), so we want LFU/LRU
 * values specific for each key. */
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;
}

上面代碼看似有三個部分,其實兩個 if 的條件時承接的,也就是隻有兩部分,第一部分是如果 Redis 的配置不要求運行 LRU 或 LFU 替換算法,並且轉換後的 value 值小於 OBJ_SHARED_INTEGERS,那麼會返回共享數字對象。Redis 在啓動服務端的時候,會自動創建 0 到 9999 共 10000 個共享整型字符串對象,如果字符串鍵值對的值在這個範圍內,就直接使用了,同時 refcount 累加 1。共享對象的好處是顯而易見的,就是節省內存,同時只能在字符串鍵的值爲數字類型時才使用,畢竟 long 類型整數才佔 8 個字節,爲了存儲他還得分配 16 位的 redisObject,有些小題大做的,一旦此類型的量上來後,內存也是有損耗的。爲什麼字符串類型的值不能用作共享對象呢,得不償失唄,想想,比較兩個字符串對象如何比較呢,得遍歷,複雜度是 2 倍的 O(N)。第二部分如果轉換後的 value 值在 long 範圍內,則直接用 ptr 存儲,因爲指針 和 long 都佔 8 個字節,因此不會有額外的內存開銷,否則創建 sdshdr 相關類型的字符串。

上面一堆解釋,說白了就兩點:對於可以用 long 型表示的整數,編碼爲 int ;浮點數或超過 long 類型的整數編碼爲 embstr 或 raw。

127.0.0.1:6379> set score 59.5
OK
127.0.0.1:6379> object encoding score
"embstr"

當字符串對象裏保存的是浮點數時,編碼爲 embstr 或 raw,看看其下面的實現。

// object.c

/* Create a string object from a long double. If humanfriendly is non-zero
 * it does not use exponential format and trims trailing zeroes at the end,
 * however this results in loss of precision. Otherwise exp format is used
 * and the output of snprintf() is not modified.
 *
 * The 'humanfriendly' option is used for INCRBYFLOAT and HINCRBYFLOAT. */
robj *createStringObjectFromLongDouble(long double value, int humanfriendly) {
    char buf[MAX_LONG_DOUBLE_CHARS];
    int len = ld2string(buf,sizeof(buf),value,humanfriendly); /* 將浮點數轉換爲字符串 */
    return createStringObject(buf,len); /* 依據長度創建 sdshdr 相關類型的字符串 */
}

浮點數雖然底層使用的是 embstr 或 raw 編碼存儲,但是仍然可以進行加減。

127.0.0.1:6379> incrbyfloat score 10
"69.5"
127.0.0.1:6379> object encoding score
"embstr"

具體實現是,會把 score 的值轉成 long float 類型,再加上 10,之後又把結果的浮點數值轉回成字符串存儲起來。

參考書籍 :

【1】redis設計與實現(第二版)
【2】Redis 5設計與源碼分析

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