《redis设计与实现》读书笔记

一 序

  在看《redis设计与实现》,书中的版本毕竟是3.0的老一些了,荣有参考意义,代码以3.2版本为准。

    字符串是Redis中最为常见的数据存储类型,其底层实现是简单动态字符串sds(simple dynamic string),是可以修改的字符串。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。但是这个key使用的结构,都是字符串类型。

书上重点介绍了sds与C语言的字符串的差别。这里只罗列下sds优点:

  • 常数复杂度获取字符串长度。
  • 杜绝缓冲区溢出。
  • 减少修改字符串长度时所需分配内存重分配次数。
  • 二进制安全。
  • 够兼容传统C字符串。

二 基础知识

2.1 字符串操作命令

  • 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

2.2 C语言有关数据类型参考

   

   假设你跟我一样不熟悉C语言,不然后面的计算部分可以略过。

三 源码分析

    字符串的实现代码在sds.c和sds.h文件中。第一眼看上去,总体来说比mysql那种少多了,都没啥打包区分下。

在介绍结果之前,先说下一开始的特点:

     为啥sds是二进制安全的,它可以存储任意二进制数据,不像C语言字符串那样以‘\0’来标识字符串结束,因为传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止 。即,当读一个字符串时,只要遇到’\0’结尾,就认为到达末尾,就忽略’\0’结尾以后的所有字符。因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了。而SDS表头的buf被定义为字节数组,因为判断是否到达字符串结尾的依据则是表头的len成员,这意味着它可以存放任何二进制的数据和文本数据,包括’\0’。

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

3.1 sds 结构

好了,接下来是看看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[];
};

跟书上的3.0的不太一样,看起来类型多了好多,先大概说一下含义:

    len 使用的长度, 不包含空终止字符

   alloc:表示字符串的最大容量,不包含Header和最后的空终止字符

    flags:表示header的类型。

   buf[] :存放真正的数据。

#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
#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的长度

上面是定义了header的操作。没太看懂怎么算的。

3.2 empstr与 raw

debug object codehole
Value at:0x7f9abb7bc100 refcount:1 encoding:embstr serializedlength:40 lru:14932997 lru_seconds_idle:2
>  set codehole abcdefghijklmnopqrstuvwxy123456789123456
OK
> debug object codehole
Value at:0x7f9abd7898b0 refcount:1 encoding:raw serializedlength:41 lru:14933009 lru_seconds_idle:4
10

     一个字符的差别,存储形式 encoding 就发生了变化。一个是 embstr,一个是 row。测试数据长度提示是40,是包含结尾的空元素的,实际长度是39.注意这是在3.0版本测试的,如果是3.2版本,应该就是44了,后面会解释区别。

    在了解存储格式的区别之前,首先了解下RedisObject结构体。因为:robj + SDS header + string +null-term是个整个存储结构。代码在server.h    

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

不同的对象具有不同的类型 type(4bit),同一个类型的 type 会有不同的存储形式 encoding(4bit)。
为了记录对象的 LRU 信息,使用了 24 个 bit 的 lru 来记录 LRU 信息。
每个对象都有个引用计数 refcount,当引用计数为零时,对象就会被销毁,内存被回收。
ptr 指针将指向对象内容 (body) 的具体存储位置。
一个 RedisObject 对象头共需要占据 16 字节的存储空间。

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


/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#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 /* Encoded as regular linked list */
#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形式存储。 (3.2之前的版本是39)

tips: robj + SDS header + string +null-term,所以版本差异就是sdshdr的不同。

3.0版本是:

struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};

所以string长度:64-header-null-term -sdshdr(int+int)=64-16-1-(4+4)=39 因为len,free都是int,占4个字节。也是我们上面例子验证过的。

对于3.2版本代码:sdshdr改成了sdshdr8,sdshdr16,sdshdr32,sdshdr64,里面属性也从int 变成uint8_t,uint16_t.。。。本身就是针对短字符串的embstr自然会使用最小的sdshdr8。对应就是:64-header-null-term -sdshdr(uint8_t * 2 + char)=64-16-1-(1+1+1)=44. 看看上面的类型占得长度图就理解了。

  

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

  3.3 长度与扩容

sds.h有基础的操作:

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

static inline size_t sdsavail(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
            return 0;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            return sh->alloc - sh->len;
        }
    }
    return 0;
}

分别是看已用长度跟可用长度。试着去理解下代码:

unsigned char flags = s[-1]。根据之前的sds结构,对于定义的sds s,把s向前移动一个字节就能获取到flag。(个人理解:声明结构体时已经取消了优化字节对齐,所以这些结构体都是程序都是连续存储的,有点类似offset 的感觉  )

那么:flags&SDS_TYPE_MASK怎么理解?

     flag前面提过了就 是0-4的取值。最大是4,二进制是100,也就是3位。

#define SDS_TYPE_MASK 7 //掩码3位最大:111就是7了
#define SDS_TYPE_BITS 3  //位数

&SDS_TYPE_MASK这样就能保证截取的值在7以内,再去进行switch判断。

后面就是做减法了,可用= 容量-已用

sdssetlen,sdsinclen,sdsalloc,都是类似的。再看看sds.c的代码:

创建字符串:

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}

/* Create a new sds string with the content specified by the 'init' pointer
 * and 'initlen'.
 * If NULL is used for 'init' the string is initialized with zero bytes.
 *
 * The string is always null-termined (all the sds strings are, always) so
 * even if you create an sds string with:
 *
 * mystring = sdsnewlen("abc",3);
 *
 * You can print the string with printf() as there is an implicit \0 at the
 * end of the string. However the string is binary safe and can contain
 * \0 characters in the middle, as the length is stored in the sds header. */
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);//根据长度觉得使用哪种sdshdr
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this.  type5在sds.h注释说不用,用8替代*/
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type); //获取当前sdshdr类型的大小,代码在上面,没贴
    unsigned char *fp; /* flags pointer. flag指针*/ 

    sh = s_malloc(hdrlen+initlen+1); //申请需要的内存,多的1个字节用于存放'\0'
    if (!init)
        memset(sh, 0, hdrlen+initlen+1); //初始化:第一个参数是位置,第三个参数是长度
    if (sh == NULL) return NULL;  //没有申请到足够内存则退出
    s = (char*)sh+hdrlen;     //得到柔性数组的地址
    fp = ((unsigned char*)s)-1;  //拿到flags的地址
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);  //根据现在的类型,去初始化sdshdr的成员信息,不包含柔性数组
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen); //将数组拷贝到柔性数组当中
    s[initlen] = '\0';  //追加\0结尾
    return s;
}

追加:

/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
 * end of the specified sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s); //就是获取sdshdr中的len属性
  //   按需调整空间,如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中
    s = sdsMakeRoomFor(s,len); 
    if (s == NULL) return NULL; //内存不足,扩容出错返回
    memcpy(s+curlen, t, len);  // 将需要添加的字符串拷贝到目的字符串尾部
    sdssetlen(s, curlen+len);  //设置追加后的sdshdr长度值
    s[curlen+len] = '\0';//追加以\0 结尾
    return s;
}
/* Enlarge the free space at the end of the sds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the sds string as returned
 * by sdslen(), but only the free buffer space we have. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    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);
    sh = (char*)s-sdsHdrSize(oldtype); //用sds(指向结构体尾部,字符串首部)减去结构体长度得到结构体首部指针
    newlen = (len+addlen);                  //获取追加后的长度
    if (newlen < SDS_MAX_PREALLOC)          //SDS_MAX_PREALLOC宏是1M内存的大小,如果小于1M,则在当前长度的基础上翻倍
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;         //如果追加后的长度超过1M,则长度追加1M个字节

    type = sdsReqType(newlen);              //根据新的长度判断对应的sdshdr类型

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);              //获取新类型的sdshdr大小
    if (oldtype==type) {
        newsh = s_realloc(sh, hdrlen+newlen+1);     //如果新旧类型一直,则在原来的地址上重新安装长度分配空间
        if (newsh == NULL) return NULL;             //分配失败则返回
        s = (char*)newsh+hdrlen;                    //获得柔性数组的地址
    } else {
        newsh = s_malloc(hdrlen+newlen+1);          //新的类型发送变化,sdshdr不一致,所以重新分配新的内存
        if (newsh == NULL) return NULL;             //分配失败则返回
        memcpy((char*)newsh+hdrlen, s, len+1);      
        s_free(sh);                                 //释放原来的sdshdr空间
        s = (char*)newsh+hdrlen;                    //获得柔性数组的地址
        s[-1] = type;
        sdssetlen(s, len);                          //设置新sdshdr的len属性
    }
    sdssetalloc(s, newlen);                         //设置新sdshdr的alloc属性
    return s;
}

   看起来真刺激,离不开找位置,计算长度之类。C语言真牛逼。 通过上面的源代码可以看出,扩容策略是字符串在长度小于 SDS_MAX_PREALLOC 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 SDS_MAX_PREALLOC 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 SDS_MAX_PREALLOC大小的冗余空间。
  还有就是惰性空间释放用于优化SDS的字符串缩短操作。就是

/* Modify an sds string in-place to make it empty (zero length).
 * However all the existing buffer is not discarded but set as free space
 * so that next append operations will not require allocations up to the
 * number of bytes previously available. */
void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

实现在sds.h


static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}

当要清空一个SDS时,并不真正释放其内存,而是设置len字段为0即可,这样当之后再次使用到该SDS时,可避免重新分配内存,从而提高效率。

先看到这里,sds.c还有大半代码没看完。

 

参考:

https://blog.csdn.net/langzi7758521/article/details/51512215

https://blog.csdn.net/qq193423571/article/details/81637075

https://blog.csdn.net/king_qg/article/details/83116760

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值