Redis-字符串

本文中展示的源码都是基于Redis7.0

SDS定义

Redis 中的字符串类型使用的是 sds,也就是 “simple dynamic string”。sds 是 Redis 自己实现的一种字符串类型,它比 C 语言中的原始字符串类型 char[] 更为高效、安全,并提供了一些额外的功能。

sds 实际上是通过一个固定结构的 sdshdr 来实现的,其中 sdshdr 是 sds 的实际存储数据结构,包含字符串长度、可用空间、引用计数等信息。

相比较于 C 语言中的原始字符串类型,sds 在以下几个方面有很大的优势:

  1. 获取字符串长度的时间复杂度为 O(1),而原始字符串类型需要遍历整个字符串才能得到长度,时间复杂度为 O(N)。

  2. 避免了缓冲区溢出和内存泄漏等常见问题。

  3. 在内存不足时,采用类似于 C++ 中的 vector 的方式进行内存分配,避免了频繁的内存分配和释放操作。

  4. 提供了字符串的动态修改功能。

sds和sdshdr定义
src/sds.h

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

通过源代码我们可以看到,sdshdr结构体定义了若干属性,具体属性说明如下:

  1. len:保存已使用的空间长度,即字符串的实际长度。在创建 sds 时,len 被初始化为 0,然后在字符串追加、修改等操作中会动态增加和减少。
  2. alloc:保存总分配的空间长度,包括实际保存字符串内容的空间和额外的空间,额外空间主要用于避免频繁调用 realloc() 函数,提升字符串操作的效率。在创建 sds 时,alloc 会根据实际需要的空间长度动态增加,一般情况下会比字符串实际长度大一些。
  3. flags:标志位,用于储存类型信息和其它特殊信息,如是否可用 realloc() 等,在不同的实现中,flags 可能保存的信息不同。
  4. buf:实际保存字符串内容的字符数组,类型为 char*,长度未指定。

编码方式

sds字符串有两种编码方式,分别是embstrraw

内存图示

embstr编码的字符串"Python"如下图所示,可以看到redisObjectsds在内存中是连续的,这样是为了字符串较短的时候,采用embstr编码就可以只分配一次内存。

raw编码的字符串如下图所示,可以看到redisObjectsds在内存中是不连续的。

决定条件

当字符串大小大于44字节会使用raw编码,否则使用embstr编码。那么为什么是44呢?

redisObject的定义
src/server.h

#define LRU_BITS 24
struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
};

这是redisObject结构体的定义,每个属性占用空间大小:

  • type为4bits
  • encoding为4bits
  • lru为24bits
  • refcount为4bytes
  • ptr为8bytes(64位系统)

做一个加法 0.5 + 0.5 + 3 + 4 + 8 = 16 0.5+0.5+3+4+8=16 0.5+0.5+3+4+8=16,也就是redisObject会占用16个字节,因为内存分配器 (如jemalloc、tcmalloc等) 使用的内存块大小通常是2的幂,也就是说分配给这个redis对象的字节数可能是32、64等。再算算sdshdr至少占用的空间,也就是sdshdr8,可以很直观看到是3个字节,所以当使用embstr编码的时候,总共至少占用19个字节,如果分配空间的时候分配32,那么实际留给字符串的空间就只有12个字节(结尾\0占一个字节),这样可能太少了。所以作者就按照64字节分配,所以得到 64 − 19 − 1 = 44 64-19-1=44 64191=44这个最终结果。

总结:embstr编码相比raw编码,少进行一次内存分配的操作,提升了效率。决定使用哪种编码的因素是字符串大小,这个阈值为44字节,这个值是根据redisObjectsdshdr占用空间算出来的。

空间分配

我们就通过源码调试来看下SDS到底是如何分配空间的(可以在这里配置调试源码Redis-调试源码)。

我们启动源码调试之前,先在这里打个断点,看下sz_index2size_tab的值,方便后续理解如何分配空间给reids字符串的。

redis/deps/jemalloc/include/jemalloc/internal/sz.h文件中sz_index2size_lookup

set字符串

1.打断点

createStringObject源码如下:

/* Create a string object with EMBSTR encoding if it is smaller than
 * OBJ_ENCODING_EMBSTR_SIZE_LIMIT, otherwise the RAW encoding is
 * used.
 *
 * The current limit of 44 is chosen so that the biggest string object
 * we allocate as EMBSTR will still fit into the 64 byte arena of jemalloc. */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

2.输入命令

输入命令set name python,在WATCH中输入c->argv[0]->ptrc->argv[1]->ptrc->argv[2]->ptr这三个值,当代码卡在断点之后,我们Continue三次之后,可以看到结果:

没错就是我们输入的命令,实际上到了这里,redis服务端已经将我们输入的命令解析为了三个sds字符串,接下来我们卡看字符串"python"的具体属性。

3.查看len和alloc

直接输入sdslen(c->argv[2]->ptr)sdsalloc(c->argv[2]->ptr)可以看看到sds的字符串"python"的lenalloc都为6

4.查看编码

此时编码格式是embstr,与我们料想的一致。

encoding为8就是对应源码中的OBJ_ENCODING_EMBSTR

src/object.c

/* 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  /* No longer used: old hash encoding. */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#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 listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */

char *strEncoding(int encoding) {
    switch(encoding) {
    case OBJ_ENCODING_RAW: return "raw";
    case OBJ_ENCODING_INT: return "int";
    case OBJ_ENCODING_HT: return "hashtable";
    case OBJ_ENCODING_QUICKLIST: return "quicklist";
    case OBJ_ENCODING_LISTPACK: return "listpack";
    case OBJ_ENCODING_INTSET: return "intset";
    case OBJ_ENCODING_SKIPLIST: return "skiplist";
    case OBJ_ENCODING_EMBSTR: return "embstr";
    case OBJ_ENCODING_STREAM: return "stream";
    default: return "unknown";
    }
}

5.设置raw字符串

如果设置编码类型为raw的SDS字符串呢?
再来一次上述步骤,不同的是这次我们设置一个长度超过44的字符串set longstr a_value_gt_44_a_value_gt_44_a_value_gt_44_a_value_gt_44,同样查看属性:

  • len: 55
  • alloc: 60

通过分析源码_sdsnewlen,如上图所示,这个sds字符产会占用hdrlen+initlen+1长度也就是 3 + 55 + 1 = 59 3+55+1=59 3+55+1=59字节的空间,此时jemalloc会分配出64bytes的空间。其中再减去sdshdr和’\0’的4bytes,也就是 64 − 4 = 60 64-4=60 644=60得到alloc为60。

append字符串

1.打断点

我们找到appendCommand方法,在里面打个断点。

然后分别执行命令:


127.0.0.1:6379> set a python
OK
127.0.0.1:6379> append a " is the best language"

可以看到终端命令卡住,应该是代码执行到了断点处,接下来就单步调试看一下具体情况。

2.单步执行

可以根据下图,最终执行到_sdsMakeRoomFor方法,接下来我们来看看这个方法。

3._sdsMakeRoomFor

源码:


sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

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

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    if (greedy == 1) {
        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);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    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);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}

代码就不详细说明了,只说下关键点:

  1. 如果原SDS剩余空间足够,则直接返回,不需要重新申请空间。
    代码:if (avail >= addlen) return s;

  2. 如果新字符串长度大小于1M,则新长度再乘2,否则新长度在原基础上加1M。
    代码:

        if (greedy == 1) {
            if (newlen < SDS_MAX_PREALLOC)
                newlen *= 2;
            else
                newlen += SDS_MAX_PREALLOC;
        }
    

    原SDS字符串“python”长度为6,增加的“ is the best language”长度为21,则newlen为27,27小于1M,所以newlen为54(本例中greedy恒为1)。

  3. 申请空间
    代码:

     newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
    

    hdrlen为3,newlen为54,所以会申请 54 + 3 + 1 = 58 54+3+1=58 54+3+1=58字节的空间,根据sz_index2size_tab实际会得到64字节的空间,接着usable = usable-hdrlen-1;得到usable为60,这里的usable其实就是新字符串的alloc属性。

4.属性查看

可以看到,与我们料想的一致,len为27,alloc为60。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值