Redis SDS

what 

--sds.h
typedef char *sds;
  • sds 即是 char * 的别名
--sds.h
/* 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[];
};

sds 一共有 5 种类型的 header

  • sdshdr5
  • sdshdr8
  • sdshdr16
  • sdshdr32
  • sdshdr64

之所以有5种,是为了能让不同长度的字符串可以使用不同规格的 header,这样,短字符串就能使用较小的规格的 header,从而节省内存。

why 

字符串长度计算

根据传统,C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符'\0'。

因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)

而SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)

通过使用SDS而不是C字符串,Redis将获取字符串长度所需的复杂度从O(N)降低到了O(1),这确保了获取字符串长度的工作不会成为Redis的性能瓶颈。

缓冲区溢出

C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)。

与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。

内存重分配

因为C字符串并不记录自身的长度,所以对于一个包含了N个字符的C字符串来说,这个C字符串的底层实现总是一个N+1个字符长的数组(额外的一个字符空间用于保存空字符)。因为C字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或者缩短一个C字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作:

  • 如果程序执行的是增长字符串的操作,比如拼接操作(append),那么在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小——如果忘了这一步就会产生缓冲区溢出
  • 如果程序执行的是缩短字符串的操作,比如截断操作(trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用的那部分空间——如果忘了这一步就会产生内存泄漏

为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf 数组的长度不一定就是字符数量加一,数组里面还可以包含未使用的字节,而未使用字节数由 allot 和 len 共同决定。

通过未使用空间,SDS实现了空间预分配惰性空间释放两种优化策略!

内存预分配

空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。

下面以sdscat函数为例子:

sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}

/* 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) {
    // 1.计算出当前sds的长度,其实返回的是sdshdr->len
    size_t curlen = sdslen(s);
    // 2.核心方法,内存助手方法,分配内存
    s = sdsMakeRoomFor(s,len);
    // 3.如果分配内存失败,则返回NULL
    if (s == NULL) return NULL;
    // 4.如果分配内存成功,则进行数据拷贝,将数据长度为len的t拷贝到s+curlen起始的位置
    memcpy(s+curlen, t, len);
    // 5.设置sdshdr->len为curlen+len
    sdssetlen(s, curlen+len);
    // 6.设置末位填充空‘\0’
    s[curlen+len] = '\0';
    return s;
}

SDS的内存预分配完全是由助手方法 sdsMakeRoomFor 实现的,我们来重点解读一下。

#define SDS_MAX_PREALLOC (1024*1024)

/* 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;
    // 1.计算出sds中还生效多少可用空间,即sdshdr->allot - sdshdr->len
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    // 2.计算出当前sdshdr的类型,即sdshdr5/sdshdr8/sdshdr16/sdshdr32/sdshdr64
    //   计算方式很简单,将sds指针前移1字节,即得到sdshdr->flags,
    //   flags是一个char类型值,低3位用于标识sdshdr类型,高5位保留未来使用
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

    // 3.如果可用空间足够足够存储addlen,则直接返回
    if (avail >= addlen) return s;
    // 4.取出sdshdr->len
    len = sdslen(s);
    // 5.取出sdshdr内存地址指针,
    sh = (char*)s-sdsHdrSize(oldtype);
    // 6.计算出最终最小内存占用大小,即sdshdr->len + addlen
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    // 7.重点 SDS_MAX_PREALLOC = 1024*1024 
    //   如果最小内存大小 < 1M,则再额外增长一倍
    //   如果最小内存大小 >= 1M,则再额外增长1M
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    // 8.根据动态预分配得到的内存占用大小,确定其sdshdr类型
    //   sdshdr8只能维护2^8大小的内存
    //   sdshdr16只能维护2^16大小的内存
    //   sdshdr32只能维护2^32大小的内存
    //   sdshdr64只能维护2^64大小的内存
    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. */
    // 9.Redis虽然声明了sdshdr5,但是并不使用,而是最小使用sdshdr8
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    // 10.核心
    //    如果新旧sdshdr类型一致,则使用realloc
    //    如果新旧sdshdr类型不一致,则使用malloc+memcpy
    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        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 */
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        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);
    }
    // 11.设置sdshdr->alloc,需要注意的是,sdsMakeRoomFor函数并未操作sdsgdr->len,因为它只负责扩容
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}

通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是保留以留作将来使用。

与此同时,SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。

/* Reallocate the sds string so that it has no free space at the end. The
 * contained string remains not altered, but next concatenation operations
 * will require a reallocation.
 *
 * 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 sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    // 1.一坨计算
    //   计算出sdshdr类型,提示一下,sds指针前一位就是sdshdr->flags
    //   计算出sdshdr内存长度
    //   计算出sdshdr->len
    //   计算出可用容量= sdshdr->alloc - sdshdr->len
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    size_t len = sdslen(s);
    size_t avail = sdsavail(s);
    sh = (char*)s-oldhdrlen;

    // 2.如果可用容量为0,短路返回
    if (avail == 0) return s;

    /* Check what would be the minimum SDS header that is just good enough to
     * fit this string. */
    // 3.计算出如果只保留sdshdr->len长度的内存,不管空闲空间的话,需要使用什么类型的sdshdr及其内存长度
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);

    /* If the type is the same, or at least a large enough type is still
     * required, we just realloc(), letting the allocator to do the copy
     * only if really needed. Otherwise if the change is huge, we manually
     * reallocate the string to use the different header type. */
    // 4.核心
    // 4.1 如果新旧sdshdr类型一致,或者变更后的类型仍然足够大(超过sdshdr8范围)
    //     则采用realloc进行内存重分配,因未缩容操作,旧sdshdr必然大于等于新sdshdr
    //     所以继续复用旧的sdshdr,长度则只保留必需的,即 oldhdrlen+len+1
    // 4.2 如果新旧sdshdr类型不一致,且变更后类型为sdshdr8/sdshdr5
    //     则采用malloc进行内存分配,memcpy内存拷贝,释放掉之前的内存,设置sdshdrx参数
    if (oldtype==type || type > SDS_TYPE_8) {
        newsh = s_realloc(sh, oldhdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
    } else {
        newsh = s_malloc(hdrlen+len+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);
    }
    // 5.重置sdshdr->alloc = len
    sdssetalloc(s, len);
    return s;
}

Binary-safe 

问:什么是二进制安全字符串?

答“”二进制安全字符串可以由任何字符(字节)组成。例如,许多编程语言使用0x00字符作为字符串结束标记,因此在这种意义上,二进制安全字符串可以由这些字符组成。

Redis字符串是一个字节序列【char *】。Redis中的字符串是二进制安全的,这意味着它们有一个已知的长度,而不是由任何特殊的终止字符决定的。

由于Redis 字符串是二进制安全的,这意味着Redis 字符串可以包含任何类型的数据,例如JPEG图片或序列化的Ruby对象。

字符串的最大长度为512兆字节。你可以在Redis中使用字符串做很多有趣的事情,例如:

  • 使用 INCR 家族中的命令将字符串用作原子计数器,如 INCR, DECR, INCRBY
  • 使用 APPEND 命令将字符串拼接起来
  • 使用 GETRANGE 和 SETRANGE 随机访问向量
  • 在小内存空间中编码大量的数据,或者使用 GETBIT 和 SETBIT 创建一个 Redis 支持的Bloom Filter

虽然SDS的API都是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。

QA

1.字符串set+expire是否原子

SET 命令是一个很复杂的命令,它的语法是:

SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
  EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

我们下面按照顺序介绍一下各个参数:

  1. SET 命令名称
  2. 键名称
  3. NX 或者 XX ,这俩是互斥的,NX 标识当 key 不存在时设置, XX 表示当 key 存在时设置
  4. GET set命令支持线查后设置新值,但是返回的值必须是字符串,否则会报错
  5. EX seconds | PX milliseconds |EXAT unix-time-seconds | PXAT unix-time-milliseconds,这里是两个值,如果是 EX 后面跟的是秒,而 PX 后跟的是毫秒,EXAT 后面则跟的unix秒,,PXAT 后面跟的是 unix 毫秒,所以 redis 是可以在一个命令内执行完set+expire的工作
  6. KEEPTTL ,set 命令其实会将失效删除的,加这个参数,我们可以抑制这种行为

Redis 服务在收到命令后,会检查 key 是否过期,然后再 set 值,最后再控制失效时间,这都是在一个命令的函数内执行完的。

2.字符串在 redis 中有几种表现形式

3种

  • 如果值是一个小于等于 20 位长的纯数字,那么使用 OBJ_ENCODING_INT,对于小于 10000 的数字,redis还会优先使用共享对象(reids 搞了10000个共享数字,0-9999)
  • 如果字符串长度小于等于44,则使用 OBJ_ENCODING_EMBSTR
  • 否则就是 OBJ_ENCODING_RAW 

3.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Redis中的SDS(Simple Dynamic Strings)使用空间预分配策略来优化字符串的增长操作。当需要对SDS进行修改并且需要扩展SDS的空间时,Redis会为SDS分配修改所需的空间,并额外分配一些未使用的空间。这样可以减少内存重分配的次数,提高性能。\[1\] SDS的空间分配策略可以杜绝缓冲区溢出的可能性。在执行修改操作之前,API会先检查SDS的空间是否满足修改所需的需求,如果不满足,则会自动将SDS的空间扩展至所需的大小,然后再执行实际的修改操作,并同时分配未使用的空间。这样可以确保SDS不会出现缓冲区溢出问题。\[2\] 总结来说,RedisSDS使用空间预分配策略来优化字符串的增长操作,并通过检查和自动扩展空间来避免缓冲区溢出问题。这些策略可以提高Redis的性能和安全性。 #### 引用[.reference_title] - *1* [RedisSDS](https://blog.csdn.net/sssxlxwbwz/article/details/123064278)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Redis底层数据结构-SDS](https://blog.csdn.net/weixin_45275107/article/details/126037496)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [redis SDS介绍](https://blog.csdn.net/Mr_Z2017512/article/details/122232918)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值