Back to Blogs
Redis
设计与实现
数据结构与对象

Redis 设计与实现:数据结构与对象

Soloman
2022-09-12

Redis 设计与实现:数据结构与对象

1 简单动态字符串(simple dynamic string,SDS)

Redis 的 简单动态字符串(Simple Dynamic String,SDS) 是 Redis 内部用于表示字符串的核心数据结构,替代了 C 语言原生的 char\* 字符串,解决了其一系列缺陷。SDS 是 Redis 中键和值最常见的数据表示方式,底层以一种结构体的形式封装。

Redis底层使用SDS作为字符串表示,与C字符串相比,SDS具有以下优点:

  • 常数复杂度获取字符串长度
  • 杜绝缓冲区移除
  • 减少修改字符串长度时所需的内存分配次数
  • 二进制安全
  • 兼容部分C字符串库函数
/* src/sds.h */

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

2 链表

链表数据结构被广泛用于Redis的各种功能实现,如列表键、发布订阅、慢查询、监视器等。

Redis的链表实现特点:

  • 每一个链表节点使用一个listNode结构表示

  • 每个链表使用一个list结构表示

  • 双端无环链表,prev和next

  • 带表头和表尾指针,head和tail

  • 带链表长度计数器,len

  • 多态

/* src/adlist.h */

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

3 字典

字典是一种保存键值对的数据结构,Redis数据库使用字典作为底层实现,而Redis字典又是用哈希表作为其底层实现。

  • 每个字典有两个哈希表,一个平时使用,一个仅在rehash时使用
  • 哈希表使用链地址法解决键冲突,同一个索引上的键值对会连接成一个单向链表
  • 当扩展或收缩哈希表时,程序会将现有哈希表的所有键值对rehash到新的哈希表,rehash是渐进式完成的
/* src/dict.h */

/* 哈希表节点 */
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
    void *metadata[];           /* An arbitrary number of bytes (starting at a
                                 * pointer-aligned address) of size as returned
                                 * by dictType's dictEntryMetadataBytes(). */
} dictEntry;

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(dict *d, const void *key);
    void *(*valDup)(dict *d, const void *obj);
    int (*keyCompare)(dict *d, const void *key1, const void *key2);
    void (*keyDestructor)(dict *d, void *key);
    void (*valDestructor)(dict *d, void *obj);
    int (*expandAllowed)(size_t moreMem, double usedRatio);
    /* Allow a dictEntry to carry extra caller-defined metadata.  The
     * extra memory is initialized to 0 when a dictEntry is allocated. */
    size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;

struct dict {
    dictType *type;

    dictEntry **ht_table[2];
    unsigned long ht_used[2];

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

4 跳跃表

跳跃表是一种有序数据结构,其每个节点维持多个指向其他节点的指针,从而能够快速访问节点。跳跃表节点查找的平均效率为O(logN),最坏为O(N),可以和平衡树相比。Redis使用跳跃表作为有序集合的底层实现之一,此外,跳跃表还在Redis集群节点中用作内部数据结构。

  • Redis跳跃表由zskiplistNodezskiplist实现,其中zskiplistNode表示跳跃表节点,zskiplist则保存跳跃表信息
  • 每个跳跃表节点的层高都是1-32之间的随机数
  • 跳跃表中的节点按照分值大小进行排序,分值相同时则按照节点对象的大小排序
/* src/server.h */

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

5 整数集合

整数集合是Redis集合的底层实现之一,而整数集合的底层实现是C数组,这个数组以有序、无重复的方式保存着集合元素。在向集合中添加一个新元素时,可能会导致整数集合升级(只能升级,不能降级),即底层数组的类型会改变,升级涉及以下步骤:

  1. 根据新元素类型,扩展整数集合底层数组的空间大小,同时也为新元素分配空间
  2. 将所有旧类型元素转换为新类型元素,并按照顺序放置到相应的位置
  3. 将新元素添加到底层数组中
/* src/intset.h */

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

6 压缩列表

压缩列表是一种为了节约内存而开发的顺序型数据结构,是Redis列表和哈希的底层实现之一,添加或者删除节点可能会涉及到连锁更新。

/* src/ziplist.c */

typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

7 Redis 对象系统

经过以上介绍,了解了Redis所用到的主要数据结构:SDS、双端链表、字典、跳跃表、整数集合、压缩列表等。但Redis不是直接使用这些数据结构来实现键值对数据库,而是用这些构建了一个对象系统,这个系统包含了五种类型的对象:字符串对象、列表对象、哈希对象、集合对象、有序集合对象,每个对象都用到了至少一种前面介绍的数据结构。Redis中每个对象都由一个redisObject结构表示:

/* src/server.h */

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

7.1 type

对于Redis保存的键值对来说,键总是一个字符串对象,值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象之一。

/* src/server.h */

/* 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. */

7.2 encoding

redisObject对象的ptr指针指向对象的底层数据结构,而具体实现的数据结构由encoding决定,即决定这个对象使用了什么数据结构作为其底层实现。每种对象都可以使用多种底层数据结构实现,从而可以在不同场景下使用不同的底层数据结构实现,能够优化对象在不同场景下的效率。

/* src/server.h */

/* 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 */

7.3 字符串对象

字符串对象的编码可以是:int、raw、embstr

  • 如果字符串对象保存的是long类型整数,编码则为int
  • 如果字符串对象保存的是字符串且长度大于32字节,编码则为raw
  • 如果字符串对象保存的是字符串且长度小于32字节,编码则为embstr。embstr是专门用于保存短字符串的一种优化编码方式
  • int、embstr编码在一定条件下会转换成raw
/* src/server.h */

#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

7.4 列表对象

列表对象的编码可以是:ziplist(压缩列表)、linkedlist(双端链表)。Redis新版本中列表对象如今已不再使用这两种数据结构,当前 Redis 列表对象编码为 quicklist,每个节点是一个 listpack,整体是一个双向链表结构——支持快速的 lpush/rpush/lpop/rpop

/* src/server.h */

#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_SKIPLIST 7  /* Encoded as skiplist */
#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 */

7.5 哈希对象

哈希对象的编码可以是:hashtable、ziplist。Redis新版本中哈希对象已不再使用ziplist。

/* src/server.h */

#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* No longer used: old hash encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */

7.6 集合对象

集合对象的编码可以是:intset、hashtable。当集合元素都是整数且集合元素不超过512个时使用intset,其他情况则使用hashtable。

/* src/server.h */

#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */

7.7 有序集合对象

有序集合的编码可以是:skiplist(跳跃表)、ziplist(压缩列表)。Redis新版本中有序集合对象已不再使用ziplist。当编码为skiplist时,有序集合对象使用zset作为其底层实现,一个zset结构同时包含一个字典和一个跳跃表。

/* src/server.h */

#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

7.8 其它

Redis对象系统带有引用计数实现的内存回收机制,当一个对象的引用计数为0时,其对应内存将被回收。

对象会记录自己最后一次被访问的时间,可用于计算对象空转时长。

Redis会共享0-9999的字符串对象,从而节约内存和优化效率。

8 Redis 相关技术文章

1.Redis 设计与实现:单机数据库的实现

2.Redis 数据库基础知识:5大数据类型对象

3.如何在Python中使用Redis