Redis中string数据结构的实现

Redis有string、hash、list、set、zset几种数据结构,这些数据结构仅仅是面向使用者的上层结构,其底层的具体实现有多种。string的内部编码有三种:raw,embstr,int,其如图所示:
在这里插入图片描述

我们可以通过type命令查看数据结构类型,object encoding命令查看内部编码:

127.0.0.1:6379> type hello
string
127.0.0.1:6379> object encoding hello
"raw"

这里我们结合redis源码,来看一下这几种底层的数据结构是如何实现的。

redisObject

Redis的设计颇有“万物皆对象”的意思,无论是string,还是hash等其他的数据类型,皆为redisObject,其结构体源码如下:

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数
    int refcount;
    // 指向实际值的指针
    void *ptr;
    
} robj;

当我们使用object encoding命令时,访问的就是该结构体内的encoding字段;ptr字段就是指向实际数据的指针。

sds(Simple Dynamic String)

sds对象是字符串的底层实现,rawembstr类型均基于此实现:

struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度,初始化为0
    int free;
     // 数据空间
    char buf[];
};

len字段用于保存字符串长度,所以用strlen求字符串长度时,直接读取此值,时间复杂度为 O(1);free字段指剩余可用的空间,新建字符串时该值为0;buf则为储存字符串的数组。

raw与embstr

raw是string的默认编码类型,embstr与其类似,创建两者的函数如下:

// 创建一个 REDIS_ENCODING_RAW 编码的字符对象
robj *createRawStringObject(char *ptr, size_t len) {
    return createObject(REDIS_STRING,sdsnewlen(ptr,len));
 }

我们可以看到,创建raw 类型字符串时,首先创建 sds 对象(sdsnewlen函数),然后将新建的redisObject 中的 ptr 字段指向该 sds 对象,共进行了两次内存分配,如图所示:
在这里插入图片描述

图来自《Redis设计与实现》

// 创建一个 REDIS_ENCODING_EMBSTR 编码的字符对象
robj *createEmbeddedStringObject(char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1); 
    struct sdshdr *sh = (void*)(o+1);
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    o->lru = LRU_CLOCK();
    sh->len = len;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
        } else {
        memset(sh->buf,0,len+1);
        }
    return o;}

与 raw 类型类似,embstr 编码类型同样由 sds 对象和 redisObject 实现;但与 raw 类型不用的是,sds 和 redisObject 是同时分配的,在内存中是连续的,如图所示:
在这里插入图片描述

图来自《Redis设计与实现》

同时,embstr 是只读的。所谓只读是指,如果embstr 对象有修改,则会转变成 raw 类型。
那么什么时候选择 raw, 什么时候是 emstr 呢?

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 39
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
            return createEmbeddedStringObject(ptr,len);
    else
            return createRawStringObject(ptr,len);
    }

OBJ_ENCODING_EMBSTR_SIZE_LIMIT这个宏定义了限制长度为39(新版本为44),即长度小于39字节的字符串采用 embstr 编码,否则为 raw。我们可以把 embstr 看作为短字符串的优化。

int

Redis中整数有共享对象的概念,也就是驻留,通过REDIS_SHARED_INTEGERS定义,值为10000:

#define REDIS_SHARED_INTEGERS 10000

当欲创建的整数值在0到10000之间时,为节约内存,并不会创建新的对象,而是增加一个引用,返回共享对象:

robj *createStringObjectFromLongLong(long long value) {
	robj *o;
	// 如果值符合范围,则不创建新对象,仅仅返回一个共享对象
	if (value >= 0 && value < REDIS_SHARED _INTEGERS) {
	    incrRefCount(shared.integers[value]);
	    o = shared.integers[value];
	 }

这里我们在 redis 客户端可以用object refcount 命令查看一个对象的引用数:

127.0.0.1:6379> set number1 100
OK
127.0.0.1:6379> object refcount number1
(integer) 2
127.0.0.1:6379> set number2 100
OK
127.0.0.1:6379> object refcount number2
(integer) 3

如果整数值属于 long 类型范围,则创建一个 REDIS_ENCODING_INT 编码的字符串对象,否则用 REDIS_ENCODING_RAW 类型的字符串保存。

//值可以用 long 类型保存
	if (value >= LONG_MIN && value <= LONG_MAX) {
            o = createObject(REDIS_STRING, NULL);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*)((long)value);
            } else {
            o = createObject(REDIS_STRING,sdsfromlonglong(value));
        }

字符串的扩容

当对字符串进行修改时,如append操作,可能会触发字符串的扩容机制。该机制的具体实现如下:

sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s);// 获取 s 目前的空余空间长度,时间复杂度 O(1)
    size_t len, newlen;
    if (free >= addlen) return s; //剩余空间足够,无需扩容
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));

    newlen = (len+addlen); //新字符串的长度

    if (newlen < SDS_MAX_PREALLOC) // SDS_MAX_PREALLOC的值为1024*1024,即 1Mb
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    // 分配内存,这里多出的 1 字节是为 "\0"准备的
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); 
   // ...
   }

可以看到,字符串的扩容的过程:

  • 初次创建字符串时,剩余空间( free 字段)为0;
  • 进行扩容时,如果新字符串小于1MB,则预分配两倍的容量;例如原有len=50,新增addlen=100,则新长度 newlen=300,最终使用情况为used(50+100)+free(150);
  • 若新字符串大于等于1MB,则预分配1MB空间;如原有len=10MB,新增addlen=50,新长度newlen=10MB+50+1MB,最终使用情况为used(10MB+50)+free(1MB)。

字符串修改后,因预分配机制的存在,可能会有空余的空间未使用,这是为了多次修改时,减少内存的分配次数以及拷贝操作次数,但这同时也增加了内存消耗。我们在使用字符串时需要清楚这一点。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值