Redis学习

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

2020-04-24  本文已影响0人  十年磨一剑1111

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

上一篇下一篇

猜你喜欢

热点阅读