美文网首页Redis学习
Redis 字符串对象及其编码详解

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

作者: 十年磨一剑1111 | 来源:发表于2020-04-24 15:27 被阅读0次

    当我们在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的大小是24个字节,所以64-24 = 44;下面写段C代码来测试下redisObj的大小:

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

    打印输出的结果是24.

    那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的源码本人也没有全部看完,如果有不对的地方欢迎各位指出,看到会及时回复。

    相关文章

      网友评论

        本文标题:Redis 字符串对象及其编码详解

        本文链接:https://www.haomeiwen.com/subject/sxmbihtx.html