Redis数据结构之SDS(四)

160 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

前置tips

接着上篇提到redis的hash结构扩容计算新的数组长度时,不妨分析下Java中HashMap的计算方式

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

原理如上图所示:将输入的值减一,分别无符号左移1,2,4,8,16位进行或操作.首先无符号左移的目的是将最存在1的最高位左移一位,保证或操作时低位也为1.如

n=9
n-1    0000 1000
n>>>1  0000 0100
|      0000 1000
=      0000 1100
n>>>2  0000 0010
|      0000 1100
=      0000 1110
n>>>4  0000 0001
|      0000 1110
=      0000 1111
n>>>8  0000 0000
|      0000 1111
=      0000 1111

从上图可知始终保证非0位为1的某个值经过计算后其后续低位始终为1. 此处为什么不进行无符号右移32位呢.因为int取值范围为4个字节,总共32个bit.由于最高位为符号位,那么只有31个bit可用.

SDS

在C语言中,字符串是以’\0’字符结尾(NULL结束符)的字符数组来存储的,通常表达为字符指针的形式(char *)。它不允许字节0出现在字符串中间,因此,它不能用来存储任意的二进制数据。 Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示,这么做的原因是:

  1. 字符串长度获取时间复杂度为o(1),而C语言获取长度需要遍历整个字符串
  2. 二进制安全,c中的字符串以数组存储,以'\0'代表结束,也就是说一旦数组中存储'\0'的字符,那么原有的字符串将会发生改变
  3. 动态扩容,sds分配长度始终是大于等于使用的长度
  4. 惰性空间释放.
SDS 组成结构

在Redis中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[];
};
  • len: 表示字符串的真正长度(不包含NULL结束符在内)。
  • alloc: 表示字符串的最大容量(不包含最后多余的那个字节)。
  • flags: 总是占用一个字节。其中的最低3个bit用来表示header的类型。header的类型共有5种,在sds.h中有常量定义。
  • buf: 表示实际存储数据的数组

首先当我们获取到一个sds时,必须要知道其底层的结构.由flags来区分.

#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

static inline char sdsReqType(size_t string_size) {
    if (string_size < 1<<5)
        return SDS_TYPE_5;
    if (string_size < 1<<8)
        return SDS_TYPE_8;
    if (string_size < 1<<16)
        return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size < 1ll<<32)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

结合上述代码可以发现不同的类型对应的长度:

  • 长度在0和2^5-1之间,选用SDS_TYPE_5类型的header。
  • 长度在2^5和2^8-1之间,选用SDS_TYPE_8类型的header。
  • 长度在2^8和2^16-1之间,选用SDS_TYPE_16类型的header。
  • 长度在2^16和2^32-1之间,选用SDS_TYPE_32类型的header。
  • 长度大于2^32的,选用SDS_TYPE_64类型的header。能表示的最大长度为2^64-1。

image.png 图中展示了两个sds字符串s1和s2的内存结构,一个使用sdshdr8类型的header,另一个使用sdshdr16类型的header.

sds的字符指针(s1和s2)就是指向真正的数据(字符数组)开始的位置,而header位于内存地址较低的方向。在sds.h中有一些跟解析header有关的宏定义.

当然,使用SDS_HDR之前我们必须先知道到底是哪一种header,这样我们才知道SDS_HDR第1个参数应该传什么。由sds字符指针获得header类型的方法是,先向低地址方向偏移1个字节的位置,得到flags字段。比如,s1[-1]和s2[-1]分别获得了s1和s2的flags的值。然后取flags的最低3个bit得到header的类型。

  • 由于s1[-1] == 0x01 == SDS_TYPE_8,因此s1的header类型是sdshdr8。
  • 由于s2[-1] == 0x02 == SDS_TYPE_16,因此s2的header类型是sdshdr16。
SDS 编码.

SDS存储不同数据会对应不同的编码encoding.取值范围:

  1. int: 存储数值类型
  2. raw: 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
  3. embstr: 当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。

image.png

embstr

44个字节如何计算的: 由于cpu缓存行大小为64字节,RedisObject元数据大小8个字节,ptr为8字节,flag为1个字节,'\0'结束符1个字节,len.alloc均为1个字节.(此处redis版本需要大于等于3.2版本). 在Redis3.2版本以前的sds结构如下:

  • len: 表示使用的长度 4个字节
  • free: 表示buf没有使用的长度.4个字节
  • buf: 数组,真正存储数据的地方,以'0'结尾 计算embstre长度为64-16-4-4-1=39.
embstr编码集只读

当设置一个embstr的编码集时,对其进行追加字符串后并未达到embstr的最大长度,直接变为raw编码集; 再对embstr编码集的字符串进行修改命令时,总会变成一个raw编码集的字符串; redis的编码转换过程不可逆 只能从小的编码转换到大的编码(不包括set)**

set a asasasasasasasasasasasasas
"OK"

object encoding a

"embstr"

append a a

object encoding a

"raw"`