Redis 字符串对象及其编码详解

当我们在redis里面保存一个键值对的时候,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另外一个对象用作键值对的值(值对象),下面先来介绍下redis 对象的结构,然后再来看下字符串对象。笔者的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;

这是源码中关于redis对象的定义,下面就每个字段含义做个简单的介绍。
1) type : 表示对象的类型,分别有字符串对象,列表对象,哈希对象,集合对象,有序集合对象,利用这个字段redis可以在命令执行之前来判断一个对象是否可以执行给定的命令。
2)encoding: 数据编码方式,总共有8种分别是(不同的版本略有不同):
a . OBJ_ENCODING_INT // long类型的整数
b. OBJ_ENCODING_EMBSTR // embstr编码的简单动态字符串
c. OBJ_ENCODING_RAW //简单动态字符串
d. OBJ_ENCODING_HT //字典
e. OBJ_ENCODING_QUICKLIST //双端列表
f. OBJ_ENCODING_ZIPLIST // 压缩列表
g. OBJ_ENCODING_INTSET //整数集合
h. OBJ_ENCODING_SKIPLIST //跳跃表

3) lru: Least Recently Used即最近最少使用,LFU(最不频繁使用的)也可以使用这个字段,LRU和LFU是两种不同的算法,它们的主要作用是当redis内存不足时淘汰那些不常使用的key。当然这需要配置,默认是不限制使用的内存的,也没有设定淘汰算法,一般情况下我们会配置同时配置maxmemory和maxmemory-policy两个参数。虽然默认是不限制使用的内存大小的,但是并不意味着程序可以无限制的使用内存,如果你的操作系统同时在运行多个程序,其中某个程序占用了全部的内存,那就会导致其他程序无法运行。

4)refcount: 引用计数,用来实现对象共享,多个key 指向同一个值对象,从而可以节约内存。
5) ptr : 无类型指针,指向真正的数据。对于不同的数据类型,redis会以不同的形式来存储。

二. 字符串对象

通常情况下我们通过set key value 就可以设置一个字符串对象(当然还有其他的命令),例如:

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"

上面设置了两个key,通过type命令可知它们都是字符串对象,不过需要注意的是键number虽然是整数,redis也会将其转换为字符串来存储。

三. 字符串的三种底层编码

redis > object encoding hello
"embstr"
redis > object encoding number
"int"
redis > set msg "这是一条消息,这是一条消息,这是一条消息,这是一条消息"
OK
redis > object encoding msg
"raw"

上面的例子里面包含了字符串对象所使用的全部编码类型,分别是:int,embstr,raw。下面来分别介绍下:
1. INT 编码
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void* 转换成long),并将字符串对象的编码设置为int。
下面摘取源码中的部分代码(object.c文件)帮助大家来理解:

//转码函数,判断对象是否能被整数编码,否则不做处理
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里面对字符串对象内部转码的方法,以此来达到节约内存的目的。小伙伴对于 len< 20 可能会有点疑惑,因为有符号的long类型的取值范围是 -2^63 - 2^63-1 这个数字的最大长度恰好是19位。
另外,当实例没有设置maxmemory限制或者maxmemory-policy设置了淘汰算法的时候,如果设置的字符串键在0-10000内的数字,则可以直接引用共享对象而不用再建立一个redisObject。注: Redis在启动后会预先建立10000个分别存储从0到9999这些数字的redisObject类型变量作为共享对象。

2. embstr编码
如果字符串对象保存的是字符串值,并且这个字符串的长度小于等于44个字节(一些老一点的版本是32个字节),那么字符串对象将使用embstr编码的方式来保存这个字符串值;如果大于44个字节将使用raw编码。下面贴一段源码片段:

//创建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个字节,那么字符串对象将使用raw编码的方式来保存这个字符串值,有小伙伴可能会比较疑惑为啥是44个字节,因为jemalloc内存分配器每次分配的内存大小都是2的整数倍,至少分配32个字节的内存,大一点就是64个字节,再大一点将使用raw 编码,由于redisObject的大小是16个字节,再加上sdshdr 的3个字节和一个字符结尾的\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个字节
    void *ptr;             //8byte     可以通过C代码验证
} robj;

//从源码可以看出sdshdr5实际是不会使用的,而能使用的最小sdshdr 就是sdshdr8了
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 1个字节
    uint8_t alloc; //1个字节
    unsigned char flags; //1个字节
    char buf[];      // 包含一个\0 结束符占1个字节
};

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

输出是8个字节
那embstr 和raw 有什么区别呢?
1.首先它们都是使用redisObject结构和sdsstr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,分别包含redisObject和sdshdr两个结构。
下面贴下sds数据结构

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

这里是源码中关于sds的定义,对于不同长度的字符串会采用不同的sds来存储,这里就不详细说了,小伙伴们有个大概了解就好。

  1. 内存释放的时候embstr编码的字符串只需要释放一次内存,而raw类型需要释放两次内存。
  2. 因为embstr这种编码的字符串数据是存放在连续的一块内存里面,和raw编码的字符串相比效率更高。

下面用三张图来分别表示这三种编码格式:


INT编码示意图.png

RAW编码.png

embstr.png

四. 实践

说了一堆理论,对于这三种编码分别在什么场景下使用小伙伴们可能还是不够了解,下面来举几个例子:
需要说明的是笔者这里没有设置maxmemory,也就是maxmemory=0
例1:

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

这里我们设置了一个字符串类型的对象,编码为int,因为120在0~10000之内,所以这里redis进行对象转码使用共享对象,不需要再次创建redisObject。
例2:

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

这里我们同样设置了一个字符串类型的对象,编码为int,只不过大于10000的数据。下面展示下gdb工具单步调试的结果:


gdb.png

那下面我们再看下createStringObjectFromLongLongForValue这个函数里面的执行步骤:


gdb2.png

从调试的结果看redis对对象进行了一次转码,由于值超出了共享对象的范围,但是在long类型的范围之内,所以仍然可以使用int编码。

例3:

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

这里我们设置了embstr编码的字符串(小于等于44个字节使用embstr),同样使用gdb工具来分析下执行的过程:


gdb3.png

这里redis没有对其进行转码,因为一方面该字符串不是long类型能表示的字符串,另外由于字符串的长度小于44个字节,并且字符串原来的编码就是embstr,所以这里不做处理。
例4:

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

虽然字符串是整数类型的,但是超出long范围,另外长度也大于44个字节,这里就没有做转码操作,而是直接返回。

总结:

本文向大家展示了redis字符串对象以及它的三种编码方式,int,embstr,raw,
1) redis在创建字符串的时候会首先根据字符串的长度来判断是创建embstr编码(长度小于等于44字节)的对象还是raw编码的对象。
2) redis内部转码(只会对raw和embstr两种格式进行转码),redis会使用tryObjectEncoding函数优化对象的编码方式 ,主要是看对象是否能被整数编码,否则不做处理。能被整数编码大致有三种情况:
前提是能被long类型表示的整数型字符串
a.当实例没有设置maxmemory限制或者maxmemory-policy设置了淘汰算法的时候,且value>0 && value <=10000的时候使用的是共享对象,这些共享对象的编码是int
b. 在不满足a的情况下,且当前对象的编码为raw编码的时候会设置为int,参考源代码:


源代码1.png

c. 在不满足a的情况下,且当前对象的编码为raw编码的时候会设置为int,参考源代码:


源代码2.png

3) 内部转码发生的时候,在使用set命令,append等命令的时候都可能会发生内部转码,比如通过一些命令使得原来的字符串发生了改变,如果原来是raw编码的后来字符串的长度缩小了可以使用embstr来编码,那这个时候就会发生转码。

先写到这里,由于redis的源码本人也没有全部看完,如果有不对的地方欢迎各位指出,看到会及时回复。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容

  • 转载:可能是目前最详细的Redis内存模型及应用解读 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据...
    jwnba24阅读 620评论 0 4
  • 前言 Redis是目前最火爆的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站...
    小陈阿飞阅读 801评论 0 1
  • 想记录一下自己这次失恋的心路历程,感觉还挺有意思的。 第一,痛苦的逃避了一个星期。同时抱有幻想还会被挽回,所以纠结...
    干锅小鱼煲阅读 127评论 2 0
  • Session3的思考题:你的产品满足了顾客什么需求,有哪些直接和潜在竞争对手? 1. 从产品直接功效的角度,提供...
    孙艺嘉阅读 493评论 0 0