Redis5源码分析之字符串结构sds(一)

本文详细解析了Redis中SDS(Simple Dynamic String)的数据结构及其内存优化策略,包括不同版本的SDS头信息设计,创建、释放和拼接字符串API的使用。重点介绍了如何通过紧凑的内存布局提高性能,并展示了SDS在字符串操作中的关键操作和内存管理技巧。
摘要由CSDN通过智能技术生成

简介

nignx和redis都是高性能服务的典型代表,其线程模型都是异步网络I/O机制实现的。redis的性能可以达到每秒10w+qps。

reids的数据结构有:字符串结构sds、跳跃表skiplist、压缩列表ziplist、字典dict、快速列表quicklist、消息队列stream等。

常用的算法:Hash常用算法times33、物理位置查找算法geohash、统计算法HyperLogLog等。

sds结构

1.redis中的字符串称为动态字符,可以存储字符串和整型数据(单个或多个char表示一个整型)。3.2之前采用的是下图的设计方式:

 (1)头部投len和free可以轻松得到字符串长度;

 (2) 内容存放到动态数组中,向上层暴露,上层可以自由处理;

 (3) 由于len存在,结尾不用“\0”终止符,保证了二进制的安全;

2.Redis 5.0中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:

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[];
};

上面结构体中4个字段具体含义:

len:buf中已使用字节数;

alloc: 除去头和终止符的长度即buf的总长度;

flags: 结构体的类型标识,只用了低3位。高5位保留;

buf: 动态数组;

看到结构体通过__packed__修饰,来进行1字节对齐。如果不进行修饰的话,按默认的4字节对齐。按照一字节对齐所需内存是9个字节存储头信息,按照3字节对齐需要是4 X 3 = 12字节。所以这样的好处是节省了内存,并且通过(char*)sh+hdrlen(hdrlen是结构体长度)得到buf地址,然后通过buf[-1]就可以简单定位到需要的类型标识符。

获取buf地址:

    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }

sds的常用操作

数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。下面将着重介绍创建、释放、拼接字符串的相关API。

创建字符串

创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。
1)创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。
2)长度计算时有“+1”操作,是为了算上结束符“\0”。
3)返回值是指向sds结构buf字段的指针。

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen); //根据字符串长度选择不同类型
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;//SDS_TYPE_5 强制转换为SDS_TYPE_8
    int hdrlen = sdsHdrSize(type); //计算不同类型所需要的头部长度
    unsigned char *fp; /* flags pointer. */ //指向flags 指针

    sh = s_malloc(hdrlen+initlen+1);
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1); //+1为了保存结束符"\0"
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen; //s指向buf指针
    fp = ((unsigned char*)s)-1; //s是动态数组buf的指针,-1即指向flags
    switch(type) {
     ......//根据不同类型初始化不同结构
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0'; //添加结束符
    return s;
}

返回的s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。

释放字符串
通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存;

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

 为了优化性能(减少申请内存的开销),SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。

void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

拼接字符串

调用sdscatlen进行扩容,其中可能涉及SDS的扩容。sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。

/* 将指针t的内容和指针s的内容拼接在一起,该操作是二进制安全的*/
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 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;

    /* 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); 
    /* type5的结构不支持扩容,所以这里需要强制转成type8*/
 
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) { /*无须更改类型,通过realloc扩大动态数组即可,注意这里指向buf的指针s被更新了*/
 
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {/*扩容后数据类型和头部长度发生了变化,此时不再进行realloc操作,而是直接重新开辟内存,拼接完内容后,释放旧指针*/
        newsh = s_malloc(hdrlen+newlen+1); //将原buf内容移动到新位置
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);/释放旧指针
        s = (char*)newsh+hdrlen; //偏移sds结构的起始地址,得到字符串起始地址
        s[-1] = type;//为falgs赋值
        sdssetlen(s, len); //为len属性赋值
    }
    sdssetalloc(s, newlen); //为alloc属性赋值
    return s;
}

具体流程如下:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知始行末

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值