Skip to main content
存储1,288 5 分钟阅读

redis(1)——数据结构

作者

redis 是最值得学习的开源项目, 1. 它广泛应用 2. 它实现了单机缓存数据库,同时支持网络访问、复制、集群、订阅等高级特性 3. redis依赖很少,数据结构、日志等基础库也是自行实现,有利于学习 4. redis代码精悍、质量高,甚至很难找到优化点。

redis 是最值得学习的开源项目,

  1. 它广泛应用
  2. 它实现了单机缓存数据库,同时支持网络访问、复制、集群、订阅等高级特性
  3. redis依赖很少,数据结构、日志等基础库也是自行实现,有利于学习
  4. redis代码精悍、质量高,甚至很难找到优化点。

本文阐释redis数据结构,redis 数据结构设计的一大特点是节省内存。原因是redis作为内存数据库需要节省内存使用(连续内存的数组能减少内存碎片)。这对架构设计同样有借鉴意义,通过节省元数据内存使用来把更多元数据/索引放在内存上,可以大大提高处理的吞吐和延迟。

数据结构

在redis的数据结构中

  1. sds 最常用做dict的key
  2. list 作为redis 内部结构使用,例如事件列表,客户端列表
  3. dict 作为数据库使用
  4. redisObject 作为数据库的value使用,object可以划分成string, list, hashtable, set, sorted set 五种基本类型
    1. string 通过sds实现
    2. list 通过quicklist实现;quicklist又是list和ziplist的组合
    3. hashtable,少量数据通过ziplist实现(减小内存碎片),大量数据通过dict实现
    4. set,整数且少量数据通过intset实现,大量数据通过dict实现
    5. sorted set,少量数据通过ziplist实现,大量数据通过dict+skiplist实现(skiplist用来保序)

sds, simple dynamic string

sds 就是动态字符串,在C char* 之上的封装,增加了常用的变量和成员函数。sds 在redis使用广泛,最常用的是作为dict的key。

sds有sdshdr8,sdshdr16,sdshdr32等。后面的8, 16, 32 表示sds的最长长度为2^8, 2^16, 2^32(128byte, 64K, 4M)。一般来说,sdshdr8和sdshdr16就够用了

一般来说

C++
/* 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[];
};
  1. buf 实际存放数据,等同于C语言的char *
  2. 获得长度不需要遍历buf,只需要将(s)-(sizeof(struct sdshdr##T))强制转型为struct sdshdr##T,然后调用sdshdr##T的方法即可

SDS_HDR(T,s) 表示就s 转型为struct sdshdr##T

C++
typedef char *sds;

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        ...

创建sds,传入C风格的char *,

  1. 通过strlen获得C风格字符串长度
  2. 根据长度确定type
  3. 构造sdsHeader
  4. memcpy(s, init, initlen); 将C风格字符串 copy到sds 的buf成员
C++
/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        ...
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));

sds还实现了trim, cmp等常用函数, 是s字符串设计的良好参考

list

双向链表, 增加了len成员变量记录链表的长度

redis 双向链表主要应用在系统结构,例如事件列表fileEvents,定时器事件列表,client列表,slave列表等

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

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

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;

dict

dict 是kv哈希表,是redis 的核心存储单元。redis 接收到的kv 就是存放在dict结构里。

每个dict对象有两个dictht,每个dictht是一个hash表。使用两个hash表是后续用来rehash。rehash是哈希表扩容,哈希表扩容期间,由于长度变大,会导致key重新排布。rehash的期望是在key重新排布期间,不影响前台IO对哈希表的读写操作。

  1. redis rehash线程和前台操作使用相同线程,可以避免对dict加锁
  2. 使用两张表,可以渐进式rehash,每次迁移一个桶。rehash期间,表1的长度是旧长度,表2是新长度。读操作需要先读2,后读1;写操作只需要写表2,删除更新操作也需要同时处理两个表。
  3. rehash时,只需要复制key和value的指针,不需要拷贝数据,因此对内存的消耗有限
C++
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

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;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

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

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint;
} dictIterator;

object

object最常见的使用是作为dict的value存在,

redis key 只有sds一种类型,value有五种类型。

  1. String字符串,支持SET, GET命令,底层使用sds实现
  2. List 有序可重复的列表,支持LPUSH, RPOP, LRANGE命令,底层使用quicklist实现
  3. Hash 键值对集合,支持HSET, HGET, HGETALL,底层使用ziplist, hashtable实现
  4. Set 无序唯一的集合,支持SADD, SMEMBER,底层使用intset, hashtable实现
  5. Sorted Set有序唯一的集合(带权重),支持ZADD, ZRANGE, ZRANK命令,底层使用ziplist, skiplist+hashtable组合结构实现

intset 只用来实现set,不用来实现sorted set,原因是sorted set的entry需要有参数作为排序的权重, intset不能支持。

其中redis的hashtable就是dict

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

redis 的常用操作命令

Plain text
# 设置key value, value 
SET key value

# value 是列表
LPUSH key value1 value2  # 左侧插入
RPUSH key value1 value2  # 右侧插入
LPOP key                 # 左侧弹出元素
RPOP key                 # 右侧弹出元素

# value是哈希表
HSET key field1 val1 field2 val2   # 批量设置<field val>
HMGET key field1 field2  # 批量获取

# value是集合
SADD key member1 member2  # 添加成员
SMEMBERS key              # 获取所有成员

# value是有序集合
ZADD key score1 member1 score2 member2  # 添加带分值的成员
ZRANGE key start stop [WITHSCORES]      # 按分值升序获取成员

object核心的成员,type,encoding,ptr

C++
typedef 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;
} robj;

encoding有10种, robj可以看成不同实现数据结构的统一接口

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

robj *createQuicklistObject(void) {
    quicklist *l = quicklistCreate();
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;
    return o;
}

robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

robj *createSetObject(void) {
    dict *d = dictCreate(&setDictType,NULL);
    robj *o = createObject(OBJ_SET,d);
    o->encoding = OBJ_ENCODING_HT;
    return o;
}

robj *createIntsetObject(void) {
    intset *is = intsetNew();
    robj *o = createObject(OBJ_SET,is);
    o->encoding = OBJ_ENCODING_INTSET;
    return o;
}

object 可以序列化到rdb,也可以被load

C++
/* Load a Redis object of the specified type from the specified file.
 * On success a newly allocated object is returned, otherwise NULL. */
robj *rdbLoadObject(int rdbtype, rio *rdb) {
    robj *o = NULL, *ele, *dec;
    uint64_t len;
    unsigned int i;

    if (rdbtype == RDB_TYPE_STRING) {
        /* Read string value */
        if ((o = rdbLoadEncodedStringObject(rdb)) == NULL) return NULL;
        o = tryObjectEncoding(o);
...

quicklist

quicklist 用来实现列表类型value。

quicklist 是一个由多个 ziplist(压缩列表)节点组成的双向链表。quicklist的节点就是 ziplist。

quicklist支持对 ziplist 节点进行 LZF 压缩, 从而节省内存。 (redis是内存数据库,节省内存的使用是它必须考虑的设计)

C++
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    // ziplist
    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;

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 : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

typedef struct quicklistIter {
    const quicklist *quicklist;
    quicklistNode *current;
    unsigned char *zi;
    long offset; /* offset in current ziplist */
    int direction;
} quicklistIter;

typedef struct quicklistEntry {
    const quicklist *quicklist;
    quicklistNode *node;
    unsigned char *zi;
    unsigned char *value;
    long long longval;
    unsigned int sz;
    int offset;
} quicklistEntry;

ziplist

quicklist的元素是ziplist, 除了quicklist, ziplist还作为元素数量少的 hashtable和sorted set的存储结构。

ziplist是一种紧凑的、连续内存存储的线性数据结构,

Plain text
+---------+--------+--------+--------+--------+---------+
| zlbytes | zltail | zllen  | entry1 | entry2 | ... | zlend |
+---------+--------+--------+--------+--------+---------+

zltail(4字节):最后一个节点的偏移量(用于快速定位尾部)。

+------------------+----------------+----------------+
| prev_entry_len   | encoding       | content        |
+------------------+----------------+----------------+

prev_entry_len 前驱节点长度,用于反向遍历
encoding 编码类型

每个entry主要由三部分组成, prev_entry_len,encoding,content。content就是一段序列,char *p。

当ziplist存放hashtable时,content是dictEntry。当ziplist存放sorted set是,content就是sds。

C++

typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previos 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;

intset

intset 整数集合用来实现元素数量较小的set,需要满足

  1. 元素均为整数
  2. 元素个数小于阈值set-max-intset-entries 512

intset 采用连续内存存储,内部元素有序排布,支持二分查找

intset 支持三种整数类型,按需动态升级:

  1. INTSET_ENC_INT16:每个元素占 2 字节(范围 -32,768 ~ 32,767)。
  2. INTSET_ENC_INT32:每个元素占 4 字节(范围 -2^31 ~ 2^31-1)。
  3. INTSET_ENC_INT64:每个元素占 8 字节(范围 -2^63 ~ 2^63-1)。
C++
// intset.h
typedef struct intset {
    uint32_t encoding;  // 编码方式(INTSET_ENC_INT16/32/64)
    uint32_t length;    // 元素数量
    int8_t contents[];  // 动态数组,按 encoding 存储整数
} intset;

intset add的都是整数

C++
/* Insert an integer in the intset */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;

skiplist和dict结合

跳跃表,和dict 合作实现有序集合

redis 通过哈希表,实现有序集合O(1)的点查询;通过跳跃表,实现集合的有序和范围查询。

哈希表存放zskiplistNode的地址即可,关键成员是sds ele和double score

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

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

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

作者

Larry

关注系统、存储与高性能计算,也记录生活与长期思考。