Redis源码阅读1-SDS

简单动态字符串(SDS)

简单动态字符串(Simple Dynamic Strings,SDS)是Redis的基本数据结构之一,用于存储字符串整型数据 。SDS兼容C语言标准字符串处理函数 ,且在此基础上保证了二进制安全。本章将详细讲解SDS的实现,为读者理解Redis的原理和各种命令的实现打下基础。

什么是二进制安全?字符串的二进制安全是指,字符串能够存储和处理任意二进制数据。C语言中的字符串就不是二进制安全的,因为它要求以'\0'作为字符串的结尾,那么0本身就无法作为字符串的数据来存储。

SDS的数据结构

在Redis中,自定了SDS来代替C语言字符串,其通过显式记录字符串的长度来保证二进制安全。

c 复制代码
struct sds {
    int len;      // buf 中已占用字节数
    int free;     // buf 中剩余可用字节数
    char buf[];   // 柔性数组存储实际数据
};

什么是柔性数组?柔性数组成员(flexible array member),只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存,即该数组的大小取决于你为该结构体分配了多少内存。

在上面简化版本的SDS结构体定义中,使用两个int类型的变量lenfree来分别记录当前buf中,使用了多少字节和剩余多少字节。看起来似乎没啥问题,对吧。

但是!!!Redis还想进一步压榨存储空间,根据存储的字符串长度的不同,设计了五种不同的sds结构体:

  • 首先需要一个flag字段来标识当前SDS采用的是五个SDS结构体中的哪一个,至少需要3个bits,因此使用一个unsigned char来存储。
  • lenalloc字段分别采用uint8_tuint16_tuint32_tuint64_t类型,来存储不同长度的字符串的总容量和占用情况
  • 其中有一个例外,sdshdr5没有lenalloc字段。该SDS结构体用于存储长度为1~31的字符串,其长度使用flags中空闲的5个bits来表示,如下图所示(榨干了...):
c 复制代码
/* SDS类型 */
#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结构体头部 */
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指针指向的是buf,而不是结构体的首地址:

这样做的目的是让SDS字符串能够兼容C语言字符串,并且我们可以通过查看sds指针之前flags字段来快速找到SDS结构体的首地址。

SDS的操作

获取sds字符串的长度:

  • 先通过sds[-1]获取到flags的值
  • 然后将使用与SDS_TYPE_MASK(0b0111)按位求与,得到s的类型
  • 每种类型从SDS结构体首地址到buf字段的距离都是固定的,因此可以很方便地找到SDS结构体首地址,返回长度即可
  • 注意SDS_TYPE_5进行了特殊操作,因为其长度存储在flags字段的高5位
c 复制代码
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;
}

创建 sds 字符串:

  • 参数init,值为NULL,将分配的内存初始化为0;值为SDS_NOINIT就不进行初始化
  • 参数initlen,即要创建的字符串的长度
  • 通过sdsReqType宏函数可以获得长度为initlen的字符串对应的类型
  • 通过sdsHdrSize宏函数可以获得类型对应的首部长度
  • 分配内存,大小为 首部长度 + 字符串长度 + 1(为了兼容C字符串,存储末尾位置'\0'
  • 之后就是根据SDS类型,设置各个字段,不多叙
  • 值得注意的是,最后返回的是指向buf的指针s,而不是SDS结构体的首地址(还是为了兼容C字符串)
c 复制代码
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);    /* 根据字符串长度获取SDS类型 */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);         /* 根据类型获取首部的长度 */
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);  /* 分配内存,+1是为'\0'留位置,兼容C字符串 */
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;           /* s指向buf */
    fp = ((unsigned char*)s)-1;     /* fp指向flags */
    switch(type) {
        case SDS_TYPE_5: {          /* sdshdr5类型,将长度放在flags中 */
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);       /* SDS_HDR_VAR用于根据类型的到首部的首地址 */
            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';
    return s;        /* 返回的是s,而不是首部的首地址 */ 
}

释放 sds 字符串:

  • 老套路,先通过sds[-1]获取到flags的值
  • 然后将其使用sdsHdrSize()函数,根据flags中的类型信息(第3位)获取对应的SDS首部长度
  • 最后通过char*指针的减法运算,得到SDS结构体的首地址,调用s_free释放掉该SDS字符串
c 复制代码
void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

拼接 SDS 字符串:

  • 委托给sdscatlen()实现拼接
  • sdscatlen()中,首先确保源字符串的内存足够存储拼接结果,然后进行拷贝、设置len字段等
c 复制代码
sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);

    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

扩容策略:

  • 简单的按需扩容,不会预分配更多的内存。
  • 新旧SDS类型相同时,无需修改其他字段,使用realloc就地分配所需内存
  • 新旧SDS类型不同时,则重新分配+拷贝,并重新设置flags中的类型信息
c 复制代码
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);  /* alloc - len = 剩余的容量 */
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);

    /* 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);
    if (oldtype==type) {            /* 新旧SDS类型不变,只需稍微增加一点内存 */
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {                        /* 新旧SDS类型改变,需要修改flags中的类型信息 */
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        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的基本结构和操作方法,其余的SDS操作函数就不用多介绍了,大差不差的。

核心问题

  1. SDS字符串如何实现二进制安全?

显式地使用alloclen字段记录当前SDS字符串的容量和长度,支持任何数据存储在SDS中。

  1. SDS字符串是如何兼容C语言的?

这就是SDS比较有趣的地方,一个sds指针(char*)指向的是柔性数组buf,而不是SDS结构体首地址,并且在SDS字符串的末尾处还额外添加了一个\0字符。因此将SDS字符串完全可以被当作C字符串传给C字符串库中的函数进行处理。

  1. sdshdr5的特殊之处是什么?

为了最大程度地压榨内存,使用sdshdr5存储长度为[1, 32)的字符串,该结构体不包含alloclen字段,而是利用flags字段中剩余的5个bits来表示字符串的长度。

相关推荐
小森林之主1 小时前
凌晨3点的闹钟:分布式定时任务设计实战
java·redis·任务调度·cron·分布式定时任务
金融支付架构实战指南2 小时前
秒杀&支付订单异步落地|Redis Stream 可靠队列实战
数据库·redis·缓存·stream·秒杀
Ze3G90nYt3 小时前
Redis 分布式锁进阶第一百二十篇
数据库·redis·分布式
无涯大者3 小时前
php中redis的简单示例学习
redis·学习·php
sbjdhjd11 小时前
Redis 主从复制、哨兵高可用与 Cluster 集群部署实验手册
运维·前端·redis·云原生·开源·bootstrap·html
Trouvaille ~13 小时前
【Redis篇】Redis 哨兵(Sentinel):高可用自动故障转移
数据库·redis·缓存·中间件·sentinel·高可用·哨兵
giaz14n9X13 小时前
Redis 分布式锁进阶第五十七篇
数据库·redis·分布式
WyCAGy8ij14 小时前
Redis 分布式锁进阶第二篇讲解
数据库·redis·分布式
学Linux的语莫17 小时前
redis的数据类型和使用
数据库·redis·缓存