Redis 数据结构的底层实现 (一) RealObject,embstr,sds,ziplist,quicklist

一.realObject

Redis使用 string list zset hash set 五大数据类型来存储键和值。在每次生成一个键值对时,都会生成两个对象,一个储存键一个储存值。redis定义了RealObject结构体表示他们

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;

1.type

redis 的对象有五种类型,分别是string list zset hash set,type就是用来标识这五种类型的。

/* Object types */
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4

(在redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象)

2.编码类型encoding

redis的对象的实际编码方式由encoding参数指定,也就是ptr指针指向的数据以何种底层实现存放。

/* 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 -- 简单动态字符串sds*/
#define OBJ_ENCODING_INT 1     /* Encoded as integer -- long类型的整数*/
#define OBJ_ENCODING_HT 2      /* Encoded as hash table -- 字典dict*/
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap -- 3.2.5版本后不再使用 */
#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list -- 双向链表*/
#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 -- embstr编码的sds*/
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists -- 由双端链表和压缩列表构成的高速列表*/

可以通过 object encoding key 指令查看值对象的编码

3.访问时间lru

表示该对象最后一次呗访问的时间,占用24bit。保存该值的目的是为了计算对象的空转时长,便于决定是否应该释放该键。

4.引用计数refcount

c语言不具备自动内存回手机制,所以为每个对象设定了引用计数。

  • 当创建一个对象时,记为1;
  • 当被一个新的程序使用时,引用计数++;
  • 不再被一个程序使用时,引用计数--;
  • 当引用计数为0时,释放该对象。回收内存。
void decrRefCount(robj *o) {
    // 引用计数为小于等于0,报错
    if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
    // 引用计数等于1,减1后为0
    // 须要释放整个redis对象
    if (o->refcount == 1) {
        switch(o->type) {
        // 依据对象类型。调用不同的底层函数对对象中存放的数据结构进行释放
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        // 释放redis对象
        zfree(o);
    } else {
        // 引用计数减1
        o->refcount--;
    }
}

5.ptr指向实际存放的对象

二、OBJ_ENCODING_RAW

RAW编码方式使用简单动态字符串sds来保存字符串对象
struct sdshdr {
    unsigned int len; //buf中已经占用的空间的长度
    unsigned int free;//buf中剩余的可用空间长度
    char buf[];//数据存放位置
};
  • 其中buf[]结束并不依赖于‘\0’,使用len判断结束,可以保存二进制流对象。其中buf[]是一个柔性数组(flexible array member 详见https://blog.csdn.net/sunlylorn/article/details/7544301)
  • 预分配,可以减少修改字符串长度增长时造成的再次分配

三、OBJ_ENCODING_EMBSTR

从Redis 3.0 版本开始,字符串引入了embstr编码方式,长度小于OBJ_ENCONDING_EMBSTR_SIZE_LIMIT的字符串将以EMBSTR方式存储。

EMBSTR方式的意思是 embedded string,字符串的空间将会和redisObject对象的空间一起分配,两者分配在同一个内存块中。

redis中内存分配使用的是jemalloc,jemalloc分配内存的时候是按照8,16,32,64作为块的单位进行飞扑的。为了保证采用这种编码方式的字符串能被jemalloc分配在同一个块(chunk)中,该块长度不能超过64,故字符串长度限制OBJ_ENCONDING_EMBSTR_SIZE_LIMIT = 64 - sizeof('\0') -sizeof(robj) -sizeof(sdshdr) =39.

这样可以有效减少内存分配的次数,提高内存分配的效率,降低碎片率。

结构同样适用 sdshdr(见上文)

四、OBJ_ENCODING_ZIPLIST

链表(list),哈希(hash),有序集合(zset)在成员较少,成员值较小的时候都采用压缩链表(ziplist)编码方式进行存储。

这里成员较少,成员值较小的标准可以通过配置项进行设置

hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

 事实上,ziplist是一个经过特殊编码的双向链表(底层是一个数组),他设计的目的是为了提高存储效率。

一个普通的双向链表,链表中每一项都会占用独立的一块内存,各个项之间用指针连接起来,这样会带来大量的内部碎片(不利于局部性原理缓存加载对应块内存),而且指针也会占用额外的内存。而ziplist将表中每一项存放在前后连续的地址空间内,它其实是一个list而不是一个链表。

area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|
size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                         ZIPLIST_ENTRY_TAIL

area        |<------------------- entry -------------------->|
            +------------------+----------+--------+---------+
component   | pre_entry_length | encoding | length | content |
            +------------------+----------+--------+---------+
  • zlbytes:4bytes   表示ziplist占用的字节总数(包括zlbytes本身占用的4个bytes)
  • zltail : 4bytes  表示ziplist表中最优一项(entry)在表中的偏移(字节数)。可以很方便的找到最后一项,从而可以进行O(1)的push和pop 
  • zllen : 2bytes  表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以能够表示的最大值就是2^16 -1。如果entry中存放的个数小于等于 2^16 -2 ,那么zllen就表示实际的数据个数,如果zllen为全1(>=2^16 -1),那么数据的个数就需要遍历整个entry(也可以取zltail偏移量计算)。 
  • entry : xbytes  表示真正存放数据的数据项,长度不固定。同时,entry也有自己的内部结构,下文会解释。
  • zlend :1bytes  ziplist 的结束标记,固定值等于255(全1)。
  • 其中 zlbytes zltail zllen等值按照低位编址实现。(实际值:0x12345678 高位编址-0x12345678 低位编址-0x78563412)

entry

  • prevrawlen:表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,秩序要向前便宜prevrawlen个字节,就能够找到前一项)。这个字段采用变长编码。
  • len:表示当前数据项的长度。采用变长编码
  • data:保存数据。

变长编码

prevrawlen: 它有两种可能,要么是1个字节,或者是5个字节

1.如果前面一个数据项字节占用小于等于253,那么prevrawlen就占用一个字节

2.如果前一个entry占用字节数大于等于254,那么第一个字节就是254,后四个字节表示一个整形值,表示prevrawlen的大小。

3.255不出现在entry的第一个字节,因为它表示结束。

len:len字段更加复杂,它根据第一个字节的不同 ,一共分为9种情况。(以下用二进制表示)

1.|00xxxxxx| - 1 byte。第一个字节最高两位是00,那么len字段占1个字节,剩余的6个bit用来表示长度,最多可以表示63.

2.|01xxxxxx|xxxxxxxx| - 2 bytes。 第一个字节最高两位是01,那么len字段占2个字节,剩余的14个bit用来表示长度,最多16383(2^14-1)。

3.|10______|xxxxxxxx|xxxxxxxx|xxxxxxxx|xxxxxxxx| - 5bytes。

第一个字节最高两位是10,len字段占5个字节,总共使用32个bit来表示长度(2-7位舍弃不用),最多表示2^32-1。

(在前三种情况下,data都是按字符串存储的,从下面一种情况开始,开始按整数来存储)。

4.|11000000| - 1 byte。 len字段占用一个字节,后面的data数据储存位2个字节的int16_t类型。

5.|11010000| - 1 byte。 len字段占用一个字节,后面的data存储为4个字节的int32_t类型。

6.|11100000| - 1 byte。 len字段占用一个字节,后面的data存储为8个字节的int64_t类型。

7.|11110000| - 1 byte。 len字段占用一个字节,后面的data存储为3个字节长的整数。

8.|11111110|  - 1 byte。len字段占用一个字节,后面的data存储为1个字节长的整数。

9.|1111xxxx|  - 1byte。这是一种特殊情况,xxxx从1到13一共13个值,就用这13种情况表示真正的数据。(0001-1101一共13个值表示0-12,这种情况下data于len字段合二为一了。)

hash与zipliist

这个ziplist一共包含33个字节。从byte[0]-byte[32],每个字节的值采用16进制表示。

总结一下,这个ziplist里存了4个数据项,分别为:

字符串:name   字符串:tielei

字符串:age     整数:20

 其实这个ziplist是通过两个hset命令创建出来的。

hset user:100 name tielei
hset user:100 age

当我们为某个key第一次执行hset key field value时,redis会创建一个hash结构,这个新创建的hash底层就是一个ziplist。

// object.c
robj *createHashObject(void) { unsigned char *zl = ziplistNew(); robj *o = createObject(OBJ_HASH, zl); o->encoding = OBJ_ENCODING_ZIPLIST; return o; }

 上面的代码负责创建一个新的hash结构,实际上,它创建了一个 type = OBJ_HASH但encoding = OBJ_ENCODING_ZIPLIST的robj对象。

(每当执行一次hset命令,插入的field和value分别作为一个新的数据项插入到ziplist中)。

当然随着数据的插入,hash的层的这个ziplist就可能会转成dict。

ziplist的插入逻辑

在ziplist的entry中插入一段新的数据,会返回一个新的ziplist,替换原来传入的旧的ziplist。因为ziplist是一段连续的地址空间,对他的追加操作,会引发内存的realloc,因此ziplist的内存位置可能会发生变化。

1.先把插入结点后面结点的prevrawlen写入新entry 然后把新结点的长度写入后一个结点的prevrawlen

2.然后计算插入后的空间大小,调用allocator的zrealloc,数据拷贝。

3.然后就是将插入位置后的数据向后挪动,插入新entry。

 

五、OBJ_ENCODING_LINKEDLIST 和 OBJ_ENCODING_QUICKLIST

在redis 3.2之前 一般的链表采用LINKEDLIST编码。

在redis 3.2版本开始,所有的链表都采用QUICKLIST编码。

两者都是使用基本的双端链表数据结构,区别是QUICKLIST每个结点的值都是使用ZIPLIST进行存储的。

// 3.2版本之前
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;
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
// 3.2版本
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned int 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 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;

什么意思呢,比如,一个包含三个结点的quicklist,如果每个结点的ziplist又包含四个数据项,那么对外表现上,这个list就总共包含12个数据项。这样的设计,实际上是对于时间和空间的一种折中。

  • linkedlist便于在表的两端进行push和pop操作,但是它的内存开销较大。首先,它的每个节点除了要保存数据之外还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,容易产生内存碎片,还容易造成抖动。
  • ziplist由于是一整块连续的内存,存储效率很高,但不利于添加和删除操作,每次都会重新realloc,尤其是当ziplist很长的时候,一次realloc造成的开销特别的大,查询的开销也特别的大。

于是quicklist集合了两个结构的有点,但多少是合理的长度呢,redis系统中用户可以自定义这个值。

list-max-ziplist-size -2

这个参数可正可负,取正值 n 的时候,这个正值表示的就是每个ziplist的长度最多不能超过n。

当取复制的时候 只能取 -1 ~ -5 这五个值,表示按照字节数来限定每个ziplist节点的长度。

  • -1 每个quicklist节点大小不能超过4Kb
  • -2 每个quicklist节点大小不能超过8Kb
  • -3 每个quicklist节点大小不能超过16Kb
  • -4 每个quicklist节点大小不能超过32Kb
  • -5 每个quicklist节点大小不能超过64Kb

节点的压缩

另外,quicklist的设计目标是用来存储很长的数据链表的,当链表很长的时候,最容易被访问的是两端的数据,中间的数据被访问的概率比较低,如果应用场景符合这个特点,list还提供了一个选项,可以把中间的数据节点进行压缩,而两边不被压缩,参数

list-compress-depth 0

就是用来完成这个设置的,数值表示两边不被压缩的节点个数

  • 其中 0 是个特殊值,表示都不压缩,这是 redis的默认值。
  • 1表示quicklist两端各有1个节点不压缩,中间的节点压缩。
  • 2表示quicklist两端各有2个节点不压缩,中间的节点压缩
  • 3......
  • 对于quicklist的压缩算法,采用LZF---一种无损压缩算法。https://www.cnblogs.com/pengze0902/p/5998843.html

quicklist的数据结构(quicklist.h)

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

/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
 * 'sz' is byte length of 'compressed' field.
 * 'compressed' is LZF data with total (compressed) length 'sz'
 * NOTE: uncompressed length is stored in quicklistNode->sz.
 * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
    unsigned int sz; /* LZF size in bytes*/
    char compressed[];
} quicklistLZF;

/* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist.
 * 'count' is the number of total entries.
 * 'len' is the number of quicklist nodes.
 * 'compress' is: -1 if compression disabled, otherwise it's the number
 *                of quicklistNodes to leave uncompressed at ends of quicklist.
 * 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned int 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;

quicklistNode结构代表quicklist的一个节点

  • prev 指向前一个节点
  • next 指向后一个节点
  • zl     数据指针。如果当前节点没有被压缩,它指向一个ziplist;否则,它指向一个quicklistLZF结构
  • sz    表示zl指向的ziplist的总大小,如果他被压缩了,那么它指压缩前的ziplist大小。
  • count表示ziplist里面包含的数据项的个数,
  • encoding 表示ziplist是否被压缩
  • container 保留字段,表示一个quicklistNode是直接存数据,还是使用ziplist存数据,或者采用其他结构类存数据。但是在目前实现中是一个固定值2,表示ziplist存数据
  • recompress 这个节点之前被压缩没有,当我们使用lindex这样的命令查看某一项本来被压缩的数据时,需要把数据暂时解压,这时就设置recompress=1  做一个标记,等空闲时候再次把数据重新压缩。
  • attemped_compress 
  • extra    其他扩展字段 目前弃用

quicklistLZF结构表示一个被压缩的ziplist。

  • sz 表示压缩后的ziplist大小
  • compress 柔性数组(flexible array member) 存放数据压缩后的ziplist字节数组

quicklist是真正的保存quicklist的结构

  • count 所有ziplist数据项的总和
  • len quicklistNode节点的个数
  • fill ziplist大小的摄者 存放 list-max-ziplist-size
  • compress 节点压缩深度 存放 list-compress-depth

 上图为一个quicklist结构图 对应的fill和compress 配置 为

list-max-ziplist-size 3
list-compress-depth 2

 其中左右两端各有两个黄色节点,是没有被压缩的,他们的数据指针zl直接指向ziplist结构。中间的蓝色节点是lzf压缩过的,zl指针指向quicklistLZF结构。

左侧头结点上的ziplist有两项数据,右侧头结点有1项。

quicklist插入

  • 头尾节点插入时,如果对应节点的ziplist大小没有超过限制,则新数据直接被插入对应ziplist;如果超过限制,那么新建一个quicklistNode节点,把待插入节点插入新的ziplist中。
  • 如果插入的位置是链表中间部分,当插入位置所在的ziplist没达到大小限制,直接插入对应ziplist;
  • 当所在的ziplist大小超过了限制,但插入的位置在ziplist两端,如果相邻节点的ziplist没有超过大小限制,那么就插入相邻节点
  • 如果相邻节点的大小超过了限制,那么新建一个quicklistNode,插入对应节点
  • 对其他情况,需要吧当前ziplist分裂成两个节点,然后在其中一个节点中插入数据。
posted @ 2019-04-15 17:26  茶饭不撕  阅读(1481)  评论(0编辑  收藏  举报