Redis—字符串和SDS

一、字符串


字符串的实现代码在sds.c和sds.h文件中。

1.1 字符串介绍


字符串是Redis中最为常见的数据存储类型,其底层实现是简单动态字符串sds(simple dynamic string),是可以修改的字符串。

它类似于Java中的ArrayList,它采用预分配冗余空间的方式来减少内存的频繁分配。

这里写图片描述

如图中所示,内部为当前字符串实际分配的空间 。其中capacity是最大容量,len是实际长度,一般要高于实际字符串长度 len。

当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。(字符串最大长度为 512M)

检测容量大小的的方法如下:

static int checkStringLength(client *c, long long size) {
    // 超出了512M,就直接报错
    if (size > 512*1024*1024) {
        addReplyError(c,"string exceeds maximum allowed size (512MB)");
        return C_ERR;
    }
    return C_OK;
}


Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。但是这个key使用的结构,都是字符串类型。

1.2 字符串常用命令

 

  • set(key, value):给数据库中名称为key的string赋予值value
  • get(key):返回数据库中名称为key的string的value
  • getset(key, value):给名称为key的string赋值value,并返回上一次的值,如果key不存在,则会创建一个新的key,返回nil。
  • mget(key1, key2,…, keyN):返回库中多个string(它们的名称为key1,key2…)的value
  • setnx(key,value):如果不存在名称为key的string,则向库中添加string,名称为key,值为value
  • setex(key, time, value):向库中添加string(名称为key,值为value)同时,设定过期时间time
  • mset(key1, value1, key2, value2,…key N, value N)给多个string赋值,名称为key i的string赋值value i
  • msetnx(key1, value1, key2, value2,…key N, value N):如果所有名称为key i的string都不存在,则向库中添加string,名称key i赋值为value i
  • incr(key):名称为key的string增1操作
  • incrby(key, integer):名称为key的string增加integer
  • decr(key):名称为key的string减1操作
  • decrby(key, integer):名称为key的string减少integer
  • append(key, value):名称为key的string的值附加value
  • substr(key, start, end):返回名称为key的string的value的子串

除了对字符串的基本操作之外,还有将字符串作为位图bitmap,下面是具体的操作命令。

  • getbit(key, offset) : 获取key中的第offset + 1位的值
  • setbit (key, offset, value) :将ket的第offset+1位位设置为value
  • bitcount (key [, start, end]):统计key中从start到end内1的个数。
  • bitpos (key, bit, [start], [end):查找到指定范围内出现的第一个bit的位置

拓展
Redis 3.2之后,为了支持bitmap(位图),提供了bitfield指令集,它提供了三个指令,get/set/incryby,使用方法如下:

  • bitfield w get u4 2:表示从第3个位开始取4位,重新组成一个无符号位的数据。
  • bitfield w get i3 2: 表示从第3个位开始取3位,重新组成一个有符号位的数据
  • bitfield w set u8 8 97: 设置从第9位开始的8位,设置为97
  • bitfield w incrby u4 2 1: # 从第三个位开始,对接下来的 4 位无符号数 +1

还可以指定位溢出策略overflow,默认是折返 (wrap),还可以选择失败 (fail) 报错不执行,以及饱和截断 (sat),超过了范围就停留在最大最小值。
bitfield w overflow sat incrby u4 2 1 一直保持最大值

二、SDS——动态字符串


Redis中简单动态字符串sds数据结构与API相关文件是:sds.h, sds.c。

SDS本质上就是char *,因为有了表头sdshdr结构的存在,所以SDS比传统C字符串在某些方面更加优秀,并且能够兼容传统C字符串。

sds在Redis中是实现字符串对象的工具,并且完全取代char*..sds是二进制安全的,它可以存储任意二进制数据,不像C语言字符串那样以‘\0’来标识字符串结束,

因为传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止 。即,当读一个字符串时,只要遇到’\0’结尾,就认为到达末尾,就忽略’\0’结尾以后的所有字符。因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了。

SDS表头的buf被定义为字节数组,因为判断是否到达字符串结尾的依据则是表头的len成员,这意味着它可以存放任何二进制的数据和文本数据,包括’\0’

SDS 和传统的 C 字符串获得的做法不同,传统的C字符串遍历字符串的长度,遇零则止,复杂度为O(n)。而SDS表头的len成员就保存着字符串长度,所以获得字符串长度的操作复杂度为O(1)。

总结下sds的特点是:可动态扩展内存、二进制安全、快速遍历字符串 和与传统的C语言字符串类型兼容。

下面就是j介绍SDS的具体结构了。

2.1 SDS结构体

 

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

上图是sds的一个内部结构的例子。图中展示了两个sds字符串s1和s2的内存结构,一个使用sdshdr8类型的header,另一个使用sdshdr16类型的header。但它们都表达了同样的一个长度为6的字符串的值:”tielei”。下面我们结合代码,来解释每一部分的组成。

sds结构一共有五种Header定义,其目的是为了满足不同长度的字符串可以使用不同大小的Header,从而节省内存。 Header部分主要包含以下几个部分: + len:表示字符串真正的长度,不包含空终止字符 + alloc:表示字符串的最大容量,不包含Header和最后的空终止字符 + flags:表示header的类型。

// 五种header类型,flags取值为0~4
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4


由于sds的header共有五种,要想得到sds的header属性,就必须先知道header的类型,flags字段存储了header的类型。假如我们定义了sds* s,那么获取flags字段仅仅需要将s向前移动一个字节,即unsigned char flags = s[-1]。

然后通过以下宏定义来对header进行操作

#define SDS_TYPE_MASK 7   // 类型掩码
#define SDS_TYPE_BITS 3    
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); // 获取header头指针
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))   // 获取header头指针
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) // 获取sdshdr5的长度


2.2 在RedisObject中,SDS的两种存储形式

详情:

> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
> debug object codehole
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1
> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1...


一个字符的差别,存储形式 encoding 就发生了变化。一个是 embstr,一个是 row。

在了解存储格式的区别之前,首先了解下RedisObject结构体。

所有的 Redis 对象都有一个 Redis 对象头结构体

struct RedisObject { 
    int4 type; // 4bits  类型
    int4 encoding; // 4bits 存储格式
    int24 lru; // 24bits 记录LRU信息
    int32 refcount; // 4bytes 
    void *ptr; // 8bytes,64-bit system 
} robj;


不同的对象具有不同的类型 type(4bit),同一个类型的 type 会有不同的存储形式 encoding(4bit)。

为了记录对象的 LRU 信息,使用了 24 个 bit 的 lru 来记录 LRU 信息。

每个对象都有个引用计数 refcount,当引用计数为零时,对象就会被销毁,内存被回收。ptr 指针将指向对象内容 (body) 的具体存储位置。

一个 RedisObject 对象头共需要占据 16 字节的存储空间。

再看一下RedisObject的10种存储格式——encoding

//这两个宏定义申明是在server.h文件中
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */


而Redis 的字符串共有两种存储方式,在长度特别短时,使用 emb 形式存储 (embedded),当长度超过 44 时,使用 raw 形式存储。

存储格式

embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的。

在字符串比较小时,SDS 对象头的大小是capacity+3——SDS结构体的内存大小至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)。

如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。而64-19-结尾的\0,所以empstr只能容纳44字节。

2.3 扩容策略和扩容

 

//这两个函数都定义在sds.c文件中

sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);  // 原字符串长度
    // 按需调整空间,如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL; // 内存不足
    memcpy(s+curlen, t, len);  // 追加目标字符串的内容到字节数组中
    sdssetlen(s, curlen+len); // 设置追加后的长度值
    s[curlen+len] = '\0'; // 让字符串以\0 结尾,便于调试打印,还可以直接使用 glibc 的字符串函数进行操作
    return s;
}

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    // 首先计算出原SDS还剩多少可分配空间
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    // 已经够用的情况下直接返回
    if (avail >= addlen) return s;

    len = sdslen(s);
    // 用sds(指向结构体尾部,字符串首部)减去结构体长度得到结构体首部指针
    // 结构体类型是不确定的,所以是void *sh
    sh = (char*)s-sdsHdrSize(oldtype);

    //扩容分配策略
    newlen = (len+addlen);
    // 如果新长度小于最大预分配长度则分配扩容为2倍
    // 如果新长度大于最大预分配长度则仅追加SDS_MAX_PREALLOC长度
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    // 字符串的长度更改了,使用对头部类型也会变化
    type = sdsReqType(newlen);

    // 由于SDS_TYPE_5没有记录剩余空间(用多少分配多少),所以是不合适用来进行追加的
    // 为了防止下次追加出现这种情况,所以直接分配SDS_TYPE_8类型
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        // 类型没变化则直接使用原起始地址重新分配下内存即可
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        // 头部类型有变化则重新开辟一块内存并将原先整个SDS拷贝一份过去
        newsh = s_malloc(hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        // 旧的已经没用了
        s_free(sh);
        s = (char*)newsh+hdrlen;
        // 配置新类型
        s[-1] = type;
        sdssetlen(s, len);
    }
    // 设置新对分配对总长度
    sdssetalloc(s, newlen);
    return s;
}


SDS_MAX_PREALLOC的容量大小定义在sds.h文件中,默认是 1024 * 1024,也就是1MB。

通过上面的源代码可以看出,扩容策略是字符串在长度小于 SDS_MAX_PREALLOC 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 SDS_MAX_PREALLOC 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 SDS_MAX_PREALLOC大小的冗余空间。

2.4 惰性空间释放策略


惰性空间释放用于优化SDS的字符串缩短操作。

当要缩短SDS保存的字符串时,程序并不立即使用内存充分配来回收缩短后多出来的字节,而是使用表头的free成员将这些字节记录起来,并等待将来使用。
源码如下:

void sdsclear(sds s) {  //重置sds的buf空间,懒惰释放
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    sh->free += sh->len;    //表头free成员+已使用空间的长度len = 新的free
    sh->len = 0;            //已使用空间变为0
    sh->buf[0] = '\0';         //字符串置空
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值