redis string类型

1、string类型使用场景
  • 计数,如点赞数、限制请求次数、pv/uv等
  • 作为key-value缓存基础数据
  • 存储session,以实现分布式下的共享session
2、数据结构

redis为c语言编写的,但是c语言没有String类型,只有cha[]类型,而且char数组在初始化的时候指定完大小后就不能再改变了。基于此,redis维护了一个自己的数据结构——SDS(Simple Dynamic String)。

SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

2.1、二进制安全

在C语言中,用\0 表示字符串的结束,如果字符串本身就有0字符,那么这个字符串就会被截断,这就是非二进制安全的。若要通过某种机制保证字符串读写时不损害其内容,则是二进制安全的。

2.2 redis实现的string二进制安全
  • Redis3.2之前

    3.2版本之前的SDS数据结构主要是通过三个字段来确定一个字符串的

    struct sdshdr {
      	int len;
      	int free;
      	char buf[];
    }
    

    其中len表示buf数组已经被使用的长度,free表示buf数组剩余的可用字节数,buf则是数据空间存储数据。len和free被称为头部,通过头部可以很方便的得到字符串的长度。

    字符串数据存放在buf数组中,SDS对上层暴露的不是SDS结构体的指针,而是指向buf数组的指针,因此上层可以想读取C字符串一样读取SDS中字符串的内容,同时也兼容了C语言处理字符串的各种函数。

    redis 3.2版本之前使用的buf数组优点在于数组是连续的,不需要额外通过指针找到字符串的位置,而是直接通过指针和偏移量就可以找到数据。但是这样设计也存在缺点:不同长度的字符串占用了相同大小的头部,显然是不太好的,一个int类型占4字节,在实际的应用中存放于redis的字符串长度往往没有这么长,每个字符串都用4个字节存储未免太浪费空间。

  • 3.0之后redis 改进(version=6.2)

在这里插入图片描述

redis根据字符串的长度分成了5种数据类型sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中sdshdr5没有了头部(len 和 free),低三位表示type,高五位预留。其他四种数据结构多了一个flags字段。

在这里插入图片描述

如上图,sdshdr5结构中,flags占1个字符,其第三位表示type,高5位表示长度,能表示的长度区间为0~31,flags后面的就是字符串的内容。

当长度大于31的字符串,1个字节存不下,那么就要将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,如下图所示:

在这里插入图片描述

四个字段的含义如下:

  • len,表示buf中已经占用的字节数;
  • alloc,表示buf中已经分配字节数,记录的是buf分配的总长度,不同于free;
  • flags表示当前结构体的类型,低三位做标识位,高5位预留;
  • buf,为真正存储数据的空间

创建SDS的大致流程是,首先计算好不同类型的头部和初始长度,然后动态分配。不过需要注意下面几种情况:

  • 创建空字符串时SDS_TYPE_5会被强制转换为SDS_TYPE_8(下面空间预分配预分配部分3.2版本之后代码中会提及);
  • 长度计算时有“+1”操作,是为了算上结束符“0”
  • 返回值是指向sds结构buf字段的指针

注意:sdshdr5的数据结构只负责存储小于32字节的字符串。一般情况下,小字符串的存储更普遍,所以redis进一步压缩了sdshdr5的数据结构,将sdshdr5的类型和长度放入了同一个属性中,也就是flags的低3位表示存储类型,高5位表示存储长度。创建空字符串时,sdshdr5会被sdshdr8替代。

注意: 源码的__attribute__((__packed__)) 。结构体会按照所有变量中最宽的基本数据类型做字节对齐,但redis使用packed修饰后,结构体则变为按1字节对齐。以sdshdr32为例,修饰前按4字节对齐大小为12 = 4(len) + 4(alloc) + 4(flags, flags原本为1byte,但此时会被填充为4byte);使用packed修饰之后按1byte对齐,也就是9 = 4(len) + 4(alloc) + 1(flags),节省了3个byte。

除了节省空间外,使用packed修饰也不需要对不同的结构类型进行处理,实现变得更为简洁。因为此时是按照1byte对齐,所以SDS创建成功后,无论是sdshdr8、sdshdr16 还是 sdshdr32,都能通过 (char*)sh+hdrlen 得到 buf 指针地址(其中 hdrlen 是结构体长度,通过 sizeof 计算得到)。修饰后,无论是 sdshdr8、sdshdr16 还是 sdshdr32,都能通过 buf[-1] 找到 flags。

3、扩容及内存分配
3.1、额外未使用空间分配策略

当SDS的API对SDS进行修改,并且需要对SDS进行空间扩展的时候,redis不仅会为SDS分配修改所必需要的空间,还会为SDS分配额外的未使用空间

额外分配的未使用空间分下面两种情况:

  • 如果对SDS进行修改后,SDS的长度小于1MB,则分配和len属性同样大小的未使用空间
  • 如果对SDS进行修改之后,SDS的长度大于1MB,则会分配1MB的未使用空间

通过预分配策略,redis减少了连续执行字符串增长操作所需的内存重分配次数,类似于以空间换时间

3.2、惰性空间释放

惰性空间释放用于优化SDS字符串缩短操作,当SDS字符串执行’缩短’操作时,程序并不会将废弃的内存进行回收,而是通过修改free值,在将来字符串再次执行扩充操作时直接复写之前废弃掉的内存中。

这种操作避免了缩短字符串所需内存时的重分配策略,并为将来可能有的增长操作提供了优化。

3.3、扩容函数

3.0版本

sds sdsMakeRoomFor(sds s, size_t addlen) { // 参数:sds(原来的,未添加新字符串之前) 字符串 s 和 扩容长度 addlen(新字符串长度)
    struct sdshdr *sh, *newsh; //定义两个 sdshdr 结构体指针
    size_t free = sdsavail(s); // 获取 s 目前空闲空间长度
    size_t len, newlen; // 定义两个长度变量,一个用于存储扩展前 sds 字符串长度,一个用于存储扩展后 sds 字符串长度

    if (free >= addlen) return s; // 如果 s 目前的剩余空闲空间已经足够,无需再进行扩展,直接返回
    len = sdslen(s); // 获取 s 目前已占用空间的长度
    sh = (void*) (s-(sizeof(struct sdshdr))); //结构体指针赋值
    newlen = (len+addlen); // 字符串数组 s 最少需要的长度
    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < SDS_MAX_PREALLOC) // 如果新长度小于 SDS_MAX_PREALLOC,那么为它分配两倍于所需长度的空间
        newlen *= 2; 
    else
        newlen += SDS_MAX_PREALLOC; // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;
    return newsh->buf;
}

3.2版本及之后

sds sdsMakeRoomFor(sds s, size_t addlen) { // 参数:sds(原来的,未添加新字符串之前) 字符串 s 和 扩容长度 addlen(新字符串长度)
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK; // s[-1]为flags值,SDS_TYPE_MASK = 7,取 & 操作可以得到flags字段的低三位进而得到type值
    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);
    // 根据新长度,为 s 分配新空间所需的大小
    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; // type 5 不支持空格,所以当初始化为空字符串时转为type 8

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) { // 扩展后type为改变
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else { // 扩展后type改变
        /* 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;
}

流程图如下:

在这里插入图片描述

4、参考文章

深度分析Redis的二进制安全

内存对齐和Redis的sds采用紧凑排列

Redis 数据结构之简单动态字符串(SDS)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值