redis内部数据结构详解

1,565 阅读13分钟

redis基本特性

  1. 非关系型的键值对数据库, 可以根据键以O(1)的时间复杂度取出或者插入关联值.

  2. redis数据存在于内存中.

  3. 键值对中的类型可以是字符串, 整型, 浮点型, 且键是唯一的.

  4. 键值对中的值类型可以是string, hash, list, set, sorted set等.

  5. redis内置了复制, 磁盘持久化, lua脚本, 事务, SSL, ACLs, 客户端缓存, 客户端代理等功能.

  6. 通过redis哨兵和redis cluster模式提高可用性.

redis数据库: redisDb

redis在非集群状态下, 具有16个db, 下面是db的结构体定义.

redisDb

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB    */
    dict *expires;              /* Timeout of keys with a timeout set    过期时间字典 */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

其中id就是这个数据库的索引, 也就是0号数据库~15号数据库这16个数据库的唯一标识.

dict字典

其中dict* dict就是字典的指针, 它指向了这个db自己的所有key空间. 而dict*这里定义了很多种, expires就代表是被设置了expire的key, blocking就代表来自于客户端对阻塞队列处理的key, ready就代表是正在等待解阻塞的push操作的key, 下面是dict的结构体.

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];// ht[0] , ht[1] =null
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

dictType字典类型

第一个成员是dictType, 因为不同的dict, 可以为它指定不同的hash函数以及其它函数, 可以看看dictType的结构体定义:

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

其中hashFunction就是该字典的hash函数指针, 还有一个比较重要的函数指针是keyCompare, 因为产生hash冲突并不只能靠取模来判断, 取模完成之后, 还得比较两个key是否相同, 因此keyCompare就是一个指向当前字典使用的比较key是否相同的函数的函数指针.

dict就好比是redis中的hashtable, 由数组+链表组成, 其中dictht实际上就是dict hashtable, 它是一个数组, 有2个元素, 这是为了实现渐进式rehash才这么做的.

dictht(dict hash table)

下面是dictht的结构体定义:

typedef struct dictht {
    dictEntry **table;
    unsigned long size; //  hashtable 容量
    unsigned long sizemask;  // size -1
    unsigned long used;  // hashtable 元素个数   used / size =1
} dictht;

其中table是一个二级指针, 二级指针其实就可以看做是一个一级指针的数组, 这个数组里面放的就是类型为dictEntry的指针, 这样就串起来了, table这个属性, 实际上就是每个链表头节点的地址数组, 而链表中的每个元素的类型, 就是dictEntry.

size就是table数组的总长度.

sizemask是size-1, 因为size一定是2的整数次方, 任何数%2的整数次方, 都可以写为任何数&2的整数次方, 因此这里专门存储一个mask.

used就是table中的元素个数.

dictEntry键值对结构体

下面我们看看dictEntry的结构体定义.

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

entry就是一个链表节点, 它具有下一个entry的指针, 以及自己的key, 因此*key就是这个键值对的key, 它是string类型, 因此是一个sds类型的结构体, 重点看一下v.

v是一个联合体结构, 联合体其实跟结构体差不多, 只不过更节省空间, 并且对内部成员数据类型有要求而已.

v中有一个void*类型的指针, 成员名是val, 大概就能猜出来, 这个指针指向的就是这个键值对的value. 实际上这个指针指向的是一个redisObject类型的结构体:

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;        //  4 bit, sting , hash
    unsigned encoding:4;    //  4 bit 
    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). 
                            *    24 bit 
                            * */
    int refcount;           // 4 byte  
    void *ptr;              // 8 byte  总空间:  4 bit + 4 bit + 24 bit + 4 byte + 8 byte = 16 byte  
} robj;

结构体使用上面的变量名:数字, 表示这个变量的位数. 因此type是半个字节, encoding是半个字节, lru是3个字节

refcount是引用数量, redis中的值以引用计数法来进行内存销毁.

*ptr就是指向真正的数据的指针了, 也就是我们平时所说的string, set, list等, 只不过还会进行进一步的封装而已.

string内部实现: sds

string的实现是一个结构体: sds(simple dynamic string), 简单动态字符串.

sds对于字符串有什么优点

首先在C语言中是没有字符串这个类型的, 但是C语言为了使用方便, 还是提供了一些字符串常量的用法, 在C语言中如果想使用字符串, 只能如下来定义:

char str1[] = "hello darkness";
char str2[] = {'h', 'e', 'l', 'l', 'l', 0};
char str2[] = {'h', 'e', 'l', 'l', 'l', '\0'};

如果直接使用字符串常量的写法: "hello"这种格式, 会默认在后面补充一个0作为字符串结尾. C语言提供了一个%s用于字符串的格式化输出, 比如printf("%s\n", str1), str1作为一个char类型的指针传递进去, 执行流程就是从str1指向的地址开始, 往后一个字节一个字节地去找字符, 直到遇到0停止.

这个逻辑本身没什么问题, 但是用到redis中就有问题了, 因为redis本身是一个第三方工具, 它无法保证使用它的这个客户端不可能在使用字符串的时候, 序列化后的二进制字节序中不存在0. 如果使用c语言原生的字符串, 将会把遇到的第一个0后面的内容全部抛弃.

因此redis自定义了一个字符串类型叫做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[];// buf[0]: z:  0101001
};
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种sds, 其中hdr5是不用的

  • attribute ((packed))

使用这个属性, 就可以让结构体中的元素变为连续的地址空间, 否则默认结构体定义的时候, 会有内存对齐的优化.

  • flags

其中1字节的flags是一个状态标志位, 标识这个sds的元信息. 之所以不叫flag而叫flags, 是因为这1个字节表示了两种状态. 在hdr5中, 高三位表示类型(hdr5), 低五位表示sds中buf[]的长度. 而在hdr8, 16, 32, 64中, 高三位依然表示各自的类型, 低五位闲置未使用.

type的宏定义如下:

#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

根据长度确认sds类型代码如下:

static inline char sdsReqType(size_t string_size) {
    if (string_size < 32)  
        return SDS_TYPE_5;
    if (string_size < 0xff) //2^8 -1 = 255  
        return SDS_TYPE_8;
    if (string_size < 0xffff) // 2^16 -1 = 65535
        return SDS_TYPE_16;
    if (string_size < 0xffffffff)  // 2^32 -1 = 42.9亿
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}
  • alloc

buf[]的空间.

  • len

buf[]中已经使用的空间.

  • buf[]

真正存储字符串的数组. 这里定义成为了一个柔性数组, 本来结构体中定义数组的时候一定指定长度, 但是如果数组是结构体的最后一个成员的话, 可以不指定长度, 这样就可以在使用的时候再去分配长度了.

string类型详解

通过对字典的分析, 我们知道, string对象是封装在一个叫做redisObject中的, 这个redisObject中有一个void*的指针变量ptr指向真正的sds, 在redisObject中, 还有一个属性type, 这个属性就是为了限制客户端命令的.

/* A redis object, that is a type able to hold a string / list / set */

/* The actual Redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

一共5种数据类型, 0就代表了string, 之所以说是限制, 比如set name darkness, 设置了一个字符串类型的name, 那么这个name是无法使用lpop这种命令的, 因为它的类型被指定位了0, 字符串.

另一个字段encoding, 是编码, redis虽然分了5大数据类型, 但是每种数据类型下面还有不同的编码, 编码的目的就是为了节省内存空间以及提升效率. 比如string就有三种编码: int, embstr, raw.

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

int编码

在客户端中使用set name 123, 之后使用type name和object encoding name, 输出如下:

image.png

可以看到, 虽然值为123, 但是类型却是string, 但是编码就是int了. 那么int编码有什么特点呢?

int编码特点

回到redisObject中, 它有一个属性void* ptr指向真正的值, 但是这样真的好吗? 在64位中, 一个指针的大小是8个字节, 8个字节是64位, 可以表示的有符号整数已经是个天文数字了, 如果一个键值对的值是一个数字, 何必要用一个8字节的指针, 再去指向呢, 这不是浪费了8个字节的空间吗? 浪费空间是其一, 如果用一个指针, 再去找另一个地址的对象, 这就是两次内存io, 不论时间空间都是下下策, 因此int编码的时候, 会把指针直接赋值为键值对的值, 这样不仅省了8字节的空间, 还少使用一次内存io, 既省空间又省时间.

embstr编码

embstr, 嵌入式string编码. 在客户端随便设置一个 set money 8.8, 再用type和encoding看看输出:

image.png

可以看到, 编码已经变为了嵌入式字符串, 嵌入式字符串有什么特点呢?

embstr编码特点

介绍embstr之前, 首先需要提一下缓存行的概念, cpu从内存中读取数据的时候, 并不是需要多少数据就读多少数据, 也不可能实现这样. cpu读取的时候, 一次会读取64字节的数据, 这是由于局部性原理.

embstr利用了cpu的这个特点, 如果一个字符串的长度小于等于44, 也就是大小在44个字节以内, 就会正好够一个缓存行的大小. 为什么呢?

在redisObject中, type4位, encoding4位, lru24位, refcount4字节, ptr8字节, 加起来就是4 + 4 + 8 = 16字节. 在255以下字节的字符串, 由sdshdr8存储, 在sdshdr8中, len8位, alloc8位, flags1字节, buf结尾0是一个字节, 加起来4个字节, 一共加起来就是20个字节, 那么64个字节就剩下44个字节了, 这也就是44的由来.

不妨测试一下, 分别使用44个字节的字符串和45个字节的字符串作为值, 会不会有不同的object encoding:

image.png

正如同分析的那样, 44长度的embstr, 被encoding为embstr, 而45长度的rawstr, 被encoding为raw, 也就是普通字符串

raw编码

长度为45(包含45)以上的字符串, 将不会做优化, 直接使用ptr指向一个随机内存空间的指针, 该指针类型为sdshdr(8, 16, 32, 64).

以上三种编码, 最终ptr指向的都是sds结构体, 只不过编码格式造成内存分配的方式不一样.

sds扩容

在sds中, 定义了len和alloc两个属性, 其中alloc就是用于记录buf[]的总长度的. redis使用了空间换时间的方式, 每次扩容都会是(原始长度 + 扩容长度) * 2.

扩容的情况仅限于append和setbit命令, 而且扩容只会越加越大, 因此sds只存在扩容, 不存在伸缩.

位图

redis中存在位图这个概念, 但是并没有位图这个数据结构, 这是因为位图是基于string实现的.

执行命令: setbit testbit 1000 1, 得到一个0的返回, 是因为setbit返回的是这个位图的某个位置的原始值.

image.png

使用strlen testbit, 得到126, 是因为操作系统中只能按字节去分配内存, 因此分配1000位的位图, 得到的是一个126字节的内存大小(1001/8 + 1 = 126, 位图下标由0开始).

image.png

使用get testbit, 可以得到数据, 但是是一个看不懂的玩意儿, 可以证明, 位图的确是由string实现的. 最后一个字节是80(1000 0000), 是小端存储的原因.

image.png

位图操作

  • setbit

上面已经介绍过了 setbit key offset value, 其中key是位图的键, 也就是string的键, offset是下标, 从0开始, value是位值, 只能取0或1. 命令执行后的返回值为当前offset之前的值.

image.png

  • getbit

getbit key offset, 得到键为key的位图的offset位置的值. 返回为0或1.

image.png

  • bitcount

bitcount key (start end), 统计键为key的位图中从start个字节到end个字节中1的个数. start和end要么都写, 要么都不写. 都不写统计全部. 如下图所示:

image.png

注意: start和end是以字节来算的, 比如输入0 1, 就是统计0字节开始到1字节的1的个数, 也就是0-16位中有多少个1. 如下图所示, 由于统计8到9字节的个数, 也就是从64到80位之间, 1的个数, 由于设置了77为1, 因此返回1.

image.png

  • bitop

op就是operate, bit操作的意思. bit操作有4种, 也就是常说的 与(and), 或(or), 非(not), 异或(xor). 其中只有非(not)操作只接受一个key, 其它的三个操作都可以接收多个key.

  1. bitop and destkey key1 [key2 key3...] 其中destkey是操作过后存入的那个位图的key

执行bitop and anddest testbitop1 testbitop2

image.png

  1. bitiop or destkey key1 [key2 key3]

执行bitop or opdest testbitop1 testbitop2

image.png

  1. bitop xor destkey key1 [key2 key3] 执行bitop xor xordest testbitop1 testbitop2

image.png

  1. bitop not destkey key 执行bitop not notdest testbitop2, 只能带一个key, 多个key会报错.

image.png

list内部实现

list数据结构在redis中使用quicklist和ziplist两种数据结构实现.

redis中的list是一个插入有序的数据结构, list的操作有lpush, rpush, lpop, rpop, lindex, llen, linsert before/after等操作, 说明它可以从后往前遍历, 也可以从前往后遍历.

实现这种数据结构, 我们一般第一个想到的就是双向链表, 但是redis并没有直接这么做. 我们可以试想一下, 如果一个list, 我们都存的是1, 2, 3这样的数值类型, 却又要使用两个8字节的指针去指向前后, 就导致本身一个要不了多大内存的数据, 却由于指针的缘故, 放大了三四倍, 这种情况称为"胖指针".

ziplist

redis使用ziplist来解决"胖指针"的问题.

robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    zl[bytes-1] = ZIP_END;
    return zl;
}

上述方法将会创建一个空的ziplist

ziplist的组成格式如下所示

image.png

  • zlbytes: 32位, 表示ziplist占用的字节总数, 本身占用4个字节.

  • zltail: 32位, 表示ziplist中最后一项(entry)的偏移字节数, 通过zltail可以直接定位到尾部节点, 可以在list中很快速地进行rpush和rpop.

  • zllen: 16位, 表示ziplist中数据项的个数(有多少个entry).

  • entry: 表示真正存放数据的数据项, 长度不定.

  • zlend: 表示ziplist的结尾, 是一个结束标记, 全为1, 无符号整数, 十进制值255.

其中每个entry的格式如下:

image.png

  • prerawlen: 前一个数据项的长度, 根据前一个数据的数据项长度是否小于254, prerawlen占据的位数不一样.
  1. 前一个数据的数据项长度小于254, 就使用1个字节, 因为1个字节无符号整数最大可以表示255, 之所以这里不能全为1是因为255已经作为ziplist的end标志了.

  2. 前一个数据的数据项长度大于等于254字节, 第一个字节则为固定的254, prerawlen将会使用5个字节来表示(实际上只有4个字节, 因为第一个字节被254占用), 那么就是4个字节来表示前一个数据项的长度.

  • len: 当前数据项的长度, 太过复杂, 分为9种情况, 对应zlentry的9种编码, 因此这里就不细说, 大概就是根据第一个字节的位不同, 去区分data的不同大小, 整体来说就是能用1个字节的绝对不使用2个字节, 能使用2个字节的绝不使用3个字节.

  • encoding: 图中未标注, 使用encoding来区分是ZIP_STR_*还是ZIP_INT_*.

  • data: 真实的数据存储. 实际上是由一个char* p的指针来指向.

介绍完ziplist, 可以看到, ziplist的高效省空间主要是因为连续的存储空间, 不会存在内存碎片, 也很容易利用缓存行, 能用1字节的data, 绝不用2字节表示, 因此非常省空间. 并且没有前后指针也能实现向前和向后遍历. 但是缺点也很明显, 因为要求太高, 所以一旦遇到插入或者删除中间节点就非常麻烦.

quicklist

redis并不是完全靠ziplist去实现list的, 还会存在于一个quicklist. 下面是quicklist的结构体定义:

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : QL_FILL_BITS;              /* fill factor for individual nodes */
    unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

quicklist的代码注释也非常清晰, 包含了核心的4个数据:

  • quicklistNode* head

指向一个quicklistNode结构体的指针, 也就是每个quicklist的头节点.

  • struct quicklistNode* next

指向一个quicklistNode结构体的指针, 也就是每个quicklist的尾节点.

  • unsigned long count

所有quicknode中的所有ziplist的所有entry的个数.

  • unsigned long len

所有quicklistNode的个数.

quicklist内部是quicklistNode, 结构体定义如下:

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
  • struct quicklistNode* prev

指向上一个quicklistNode的指针.

  • struct quicklistNode* next

指向下一个quicklistNode的指针.

  • unsigned char* zl

指向当前quicklistNode中包含的ziplist的指针.

  • unsigned int sz

当前quicklistNode中ziplist的zlentry个数.

hash的内实现: dict和ziplist

ziplist编码格式

先看一个现象: 使用hset user name darkness age 28 gender 1 height 175创建一个hash的数据, 再使用hgetall看看结果:

image.png

发现所有的kv都是按照我们设置的顺序出现的, 这并非巧合. 然后使用object encoding看看当前user的编码格式:

image.png

格式为ziplist, 这是因为当前user这个hash的编码格式是ziplist, ziplist前面已经介绍过了, 下面主要看看这个ziplist中的zlentry具体的数据存储结构:

image.png

可见, 每个kv都按顺序排列在一个顺序的存储空间中, 使用zlentry去保存.

hashtable编码格式

接下来执行下面的操作: hset user image aaaaaaaaaa...aaa(超过64个a)

image.png

再使用hgetall, 会发现字段的顺序都变了, 这时候使用object encoding user会发现, 此时的编码格式已经变为了hashtable:

image.png

hashtable编码格式其实就和redisDb差不多.

ziplist转hashtable

redis设定:

  1. 在ziplist的zlentry个数大于512时, 将从ziplist转为hashtable, 可配置, 对应配置:
# ziplist 元素个数超过 512 ,将改为hashtable编码
hash-max-ziplist-entrie 512
  1. ziplist中单个entry中的元素值大小超过64字节时, 将转为hashtable存储, 可配置, 对应配置:
# 单个元素大小超过 64 byte时,将改为hashtable编码
hash-max-ziplist-value 64

set的内部实现

set是无序的, 可以自动去重的数据类型, 底层实现为一个value为null的字典(dict).

intset编码

如果set集合中的数据可以使用整数类型表示, 那么编码格式为intset.

执行sadd testset 1 2 3 4 5 6 7 7 8 9 9 11 11 11 13 13, 使用smembers testset查看所有元素.

image.png

使用object encoding testset, 发现编码格式为intset:

image.png

set为intset编码时的有序性

intset结构体定义如下:

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
/* Note that these encodings are ordered, so:
 * INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

根据注释可以看到, intset编码时, 确实是有序的, 并且我们在smembers命令的时候, 确实发现所有的整数都被排好序展示了出来.

编码为intset时, 所有的元素处于一块连续的内存空间中.

image.png

hashtable编码

当如下两个条件满足其一时, 将从intset编码转为hashtable编码:

  1. 任意一个元素无法使用整型表示:

image.png

  1. 元素个数大于512(默认), 可配置, 配置参数如下:
# intset 能存储的最大元素个数,超过则用hashtable编码
set-max-intset-entries 512

将此参数改为5, 重启redis, 证实一下上述结论:

image.png

操作结果发现, 当set元素个数为5的时候, 还是intset编码, 但是再向里面add一个不同的元素, 使得元素个数为6, 大于了5, 编码格式变为hashtable.

zset的内部实现

zset由dict和skiplist组合实现.

zset编码类型有ziplist和skiplist.

当元素个数较少时, 采用ziplist, 和hash结构一样, 只不过key是score, 值是元素值.

当单个元素大小超过ziplist的阈值, 或者元素个数超过ziplist的阈值, 将会转为skiplist.