redis underlying kernel analysis

1, Redis data structure

Redis underlying data structure comprising a simple dynamic strings (SDS), lists, dictionaries, compressed list, like the set of integers; five data types (objects) are composed of one or more of the number of structures.

You can use the command line OBJECT ENCODING key to view the data structure of the key.

2, a simple dynamic string SDS

 redis is written in C, but does not use string data type string C language, but rather a simple rewrite dynamic string (simple dynamic string, SDS).

1 / * 
 2 * save string object structures 
 . 3 * / 
 . 4 sdshdr {struct 
 . 5    
 length 6 // buf in occupied space 
 . 7 int len; 
 . 8    
 length 9 // buf remaining free space 
10 int Free; 
. 11   
12 is // data space 
13 is char buf [] 
14};

 

Saving string using SDS Redis, specifically as follows:

                                              Pictures from "Redis Design and Implementation" with Huang Jianhong

 

    • It represents the number of free space remaining in the array buf
    • Record the length len bytes in buf it has been stored in array
    • char buf array is a type of data, specific character string records stored, and the '\ 0' (null character) as the end identifier

 SDS-defined string almost the same compared with the C language, is more than two properties free, len; then why does not directly use the C language string it?

1, Get string length complexity is O (1)

        Since the C language does not store the length of the string, the string length of the multi every acquisition cycle required the entire string, time complexity is O (N); the length of the string is recorded SDS stored directly Get string length Get property value to len, the time complexity is O (1); and update the length of SDS is provided in the API automatically, without manual operation.

2, to prevent buffer overflow

 When performing two C language string concatenation, once enough memory space is not allocated, it will cause an overflow; and SDS when modifying the string, depending on the value for len will first check the memory space is sufficient, if the shortage will be assigned memory space, and then modify the string, so put an end to a buffer overflow.

3, when brought modifications to reduce the number of strings of memory reallocation

C language string length is not recorded, when the modified, re-allocate memory; string if it is normal, is not enough memory overflow occurs; If the string is shortened, no leakage will produce heavy distribution.

SDS spatial pre-allocating two optimization strategy and an inert free space

Pre-allocating space: growing the string operations will be dispensed extra unused space, so that if future expansion, to some extent, frequency and reallocation of memory can be reduced.

Inert free space: After shortened string operation, and does not release these spaces immediately, but the use of free space to record the number of these, when the growth operation, the space of these records can be reused; Providing response SDS API to manually free up space, so it will not cause a waste of memory.

4, binary safe

C language string can not contain null characters (because the C language is a null character to determine end of the string), so you can not save some binary files (may contain null characters, such as pictures); SDS is based on len to judge character end of the string, so that the structure can store pictures SDS, and are processed in binary mode.

5, string functions compatible portion C

SDS buf structure is stored null-terminated character string of the same, can be partially compatible with the operating string API C language.

to sum up:

                                             Table Source: "Redis Design and Implementation"

 

3, the list

Redis is written in C, but no built-in list this data structure, but their build a linked list implementation; nodes form a linked list structure for the list node.

List with a very wide range, such as a list of keys, publish and subscribe, slow queries, monitor and so on.

Typedef struct {listnode. 1 
2 // node front 
. 3 * struct listnode PREV; 
. 4 // post node 
. 5 * struct listnode Next; 
. 6 // value of the node 
. 7 void * value; 
. 8} listnode;

It may constitute a plurality of double-ended listNode prev and next pointers linked list by using the list hold list

Typedef struct {List. 1 
 2 // header node 
 . 3 * listnode head; 
 . 4 // End Node Table 
 5 listNode * tail; 
 the number of nodes contained in the list // 6 
 . 7 unsigned Long len; 
 . 8 // node value replicated function 
 9 void * (DUP *) (void * PTR); 
10 // node value of the release function 
. 11 void (Free *) (void * PTR); 
12 is the value of the contrast function // node 
13 int (* match) (void * ptr, void * Key); 
14} List;
    • head table pointer
    • tail tail pointer table
    • len chain length counter
    • Type specific functions required dup, free, match polymorphic list

Redis list of characteristics to achieve the following:

1, double-ended

Prev and next list node having a pointer, rapid access to pre- and post-nodes, the time complexity is O (1).

2, acyclic

 Prev head node pointer and the tail pointer to point to next node null, access to the list to NULL as the end point.

3, with a table pointer and tail pointer table

You can quickly obtain the header node and tail node table.

4, chain length counter

You can quickly get the length of the list. 

5, polymorphic

List a variety of different types of values ​​can be saved by the list dup, free, match the design values ​​of three attributes for the node type specific functions.

 

4, dictionary

Dictionary also called a symbol table (symbol table), an associative array (associative array) or map (map); key-value dictionary stored key-value pair and key will not be repeated;

Redis is widely used in the dictionary, such as Redis database is to use the dictionary as the underlying implementation.

Redis C language used is not built such a structure, so Redis build your own dictionary to achieve.

Using a hash table as dictionary again at the bottom, a hash table containing a plurality of hash nodes, each hash save a node pairs.

Hash table

Typedef struct {dictht. 1 
 2 // hash table array 
 . 3 dictEntry ** Table; 
 . 4 // size of the hash table 
 . 5 Long unsigned size; 
 . 6 // hash table size mask is used to calculate an index value 
 7 // always . 1-equal size 
 . 8 unsigned Long sizemask; 
 number 9 // the hash table has node 
10 unsigned Long Used; 
. 11} dictht;

FIG blank is a size of the hash table 4

    • table is an array, the array elements is a pointer dictEntry structure, each of the stored key-value pair dictEntry
    • The size of the hash table size record
    • sizemask 值总是等于size-1,这个属性和哈希值一起决定一个键应该被方法table数组的哪个索引上
    • used 记录哈希表目前已有节点的数量

哈希表节点 

 1 typedef struct dictEntry {
 2     // 键
 3     void *key;
 4     // 值
 5     union{
 6         void *val;
 7         uint64_tu64;
 8         int64_ts64;
 9     } v;
10     // 指向下个哈希表节点,形成链表
11     struct dictEntry *next;
12 } dictEntry;
    • key属性保存着键值对中的键,v属性保存着键值对中的值
    • 键值对中的值可以使指针val、一个uint64_t整数,或是一个int64_t整数
    • next是指向另一个哈希表节点的指针,用以解决多个哈希值冲突问题

下图为将两个索引值相同的键连在一起

 

字典结构

 1 typedef struct dict {
 2     // 类型特定函数
 3     dictType *type;
 4     // 私有数据
 5     void *privdata;
 6     // 哈希表
 7     dictht ht[2];
 8     // rehash索引
 9     //当rehash不在进行时,值为-1
10     in trehashidx; /* rehashing not in progress if rehashidx == -1 */
11 } dict;
12 
13 typedef struct dictType {
14     // 计算哈希值的函数
15     unsigned int (*hashFunction)(const void *key);
16     // 复制键的函数
17     void *(*keyDup)(void *privdata, const void *key);
18     // 复制值的函数
19     void *(*valDup)(void *privdata, const void *obj);
20     // 对比键的函数
21     int (*keyCompare)(void *privdata, const void *key1, const void *key2);
22     // 销毁键的函数
23     void (*keyDestructor)(void *privdata, void *key);
24     // 销毁值的函数
25     void (*valDestructor)(void *privdata, void *obj);
26 } dictType;
    • type 属性是一个指向dictType结构的指针,每个dictType机构保存了一簇用于操作特定类型键值对的函数,Redis货位用途不同的字典设置不同的类型特定函数。
    • privdata 属性保存了需要传给那些类型特定函数的可选参数。
    • ht 属性是一个长度为2的数组,数组中的每个元素都是一个哈希表,一般情况下自字典只使用ht[0],ht[1]只会在进行rehash时使用.
    • trehashidx 属性记录了rehash目前的进度,如果没有进行rehash则它的值为-1。

下图为普通状态下的字典结构

当一个新的键值对要添加到字典中去时,会涉及到一系列的操作,如计算索引、解决冲突、扩容等等,下面对这些操作进行描述。

1、哈希算法

添加键值对时,首先要根据键值对的键计算出哈希值和索引值,然后再根据索引值进行放入

1 #使用字典设置的哈希函数,计算键key的哈希值
2 hash = dict->type->hashFunction(key);
3 #使用哈希表的sizemask属性和哈希值,计算出索引值
4 #根据情况不同,ht[x]可以是ht[0]或者ht[1]
5 index = hash & dict->ht[x].sizemask;

2、结局键冲突

当有两个或以上数量的键值被分配到了哈希表数组的同一个索引上时,就发生了键冲突。

Redis的哈希表使用单向链表解决键冲突问题,每个新的键总是添加到单项链表的表头。

3、rehash(扩展或收缩)

哈希表具有负载因子(load factor),其始终需要保持在一个合理的范围之内,当hashI表保存的键值对过多或过少时,就需要对哈希表进行rehash(重新散列)操作,步骤许下

(1) 为字典的ht[1]分配空间,空间大小:如果是扩展操作则为ht[0].used * 2 ,也就是扩展为当前哈希表已使用空间的1倍;如果是收缩,则减小1倍。

(2) 将ht[0]内的数据重新计算哈希值和索引,并放到新分配的ht[1]空间上。

(3) 全部迁移完成后,将ht[1]设置为ht[0],释放ht[0]并创建一个空白的哈希表为ht[1],为下次rehash做准备。

4、哈希表的扩展与收缩触发条件

(1) 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等等于1。

(2) 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。

以上条件中任意一条被满足,程序自动开始对哈希表进行扩展;

负载因子算法:负载因子 = 哈希表以保存的节点数量 / 哈希表大小

当负载因子小于0.1时,程序自动进行收缩操作。

5、渐进式rehash

渐进式rehash就是,当ht[1]的键值对向ht[1]迁移的过程中,如果数据量过大,则不能一次性迁移, 否则会对服务器性能造成影响,而是分成多次,渐进式的进行迁移。

在rehash期间,会维持一个索引计数器rehashidx,并把每次的迁移工作分配到了添加、删除、查找、更新操作中,当rehash工作完成后rehashidx会增加1,这样所有的ht[0]的值全部迁移完成后,程序会将rehashidx这是为-1,标识最终的rehash完成。

6、渐进式rehash之情期间的表操作

由于渐进式rehash期间,ht[0]和ht[1]中都有数据,当查找时,会先在ht[0]中进行,没找到继续到ht[1]中找;而添加操作一律会添加到ht[1]中。

 

字典总结: 

Redis字典底层机构实现与java(1.6之前) 中的hashmap非常相像,都是使用单项链表解决键冲突问题。

个人疑问:jdk1.8以上已经是用红黑树解决多个键冲突问题,不知redis的键冲突是否也可以用红黑树?

 

5、跳跃表

跳跃表(skiplist)数据结构特点是每个节点中有多个指向其他节点的指针,从而快速访问节点。

跳跃表结构由跳跃表节点(zskiplistNode)和zskiplist两个结构组成

跳跃表节点

 1 typedef struct zskiplistNode {
 2     // 层
 3     struct zskiplistLevel {
 4         // 前进指针
 5         struct zskiplistNode *forward;
 6         // 跨度
 7         unsigned int span;
 8     } level[];
 9     // 后退指针
10     struct zskiplistNode *backward;
11     // 分值
12     double score;
13     // 成员对象
14     robj *obj;
15 } zskiplistNode;
    • 层:为一个数组,数组中的每个数据都包含前进指针和跨度。
    • 前进指针:指向表尾方向的其他节点的指针,用于从表头方向到表尾方向快速访问节点。
    • 跨度:记录两个节点之间的距离,跨度越大,两个节点相聚越远,所有指向NULL的前进指针的跨度都为0。
    • 后退指针:用于从表尾节点向表头节点访问,每个节点都有后退指针,并且每次只能后退一个节点。
    • 分值:节点的分值是一个double类型的浮点数,跳跃表中的说有分值按从小到大排列。
    • 成员对象:是一个指向字符串的指针,字符串则保存着一个SDS值。

跳跃表

1 typedef struct zskiplist {
2     // 表头节点和表尾节点
3     structz skiplistNode *header, *tail;
4     // 表中节点的数量
5     unsigned long length;
6     // 表中层数最大的节点的层数
7     int level;
8 } zskiplist;

    • header 指向跳跃表的表头节点,tail指向跳跃表的表尾节点,level记录节点中的最大层数(不含表头节点),length跳跃表包含节点数量(不含表头节点)。
    • 跳跃表由很多层构成(L1、L2 ...),每个层都带有两个属性前进指针和跨度。
    • 每个节点都包含成员对象(obj)、分值(score)、后退指针(backward),头结点也包含这些属性但不会被用到

在此处只是介绍跳跃表的结构相关,关于跳跃表的层的形成,对象的插入、删除、查询等操作的原理在此处不做详解,另外会有文章进行说明。

 

6、整数集合

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数元素,并且元素的个数不多时,Redis就会使用整数集合作为集合键的底层实现。

整数集合可以保存int16_t、int32_t、int64_t的整数值,并且不会出现重复元素

1 typedef struct intset {
2     // 编码方式
3     uint32_t encoding;
4     // 集合包含的元素数量
5     uint32_t length;
6     // 保存元素的数组
7     int8_t contents[];
8 } intset;
    • contents数组存储的是集合中的每个元素,他的类型是int8_t,但存储数据的实际类型取决于编码方式encoding
    • encoding编码方式有三种INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64分别对应的是int16_t、int32_t、int64_t类型
    • length记录整数集合的元素数量,即contents数组的长度

整数集合的升级操作

整数集合中原来保存的是小类型(如:int16_t)的整数,当插入比其类型大(如:int_64_t)的整数时,会把整合集合里的元素的数据类型都转换成大的类型,这个过程称为升级

升级整数集合并添加新元素步骤如下:

(1)根据新元素的类型,扩展整数集合的底层数据的空间大小,并为新元素分配空间。

(2)将现有的所有元素的类型转换成与新元素相同的类型,保持原有数据有序性不变的情况下,把转换后的元素放在正确的位置上。

(3)将新元素添加到数组里。

新元素引发升级,所以新元素要么比所有元素都大,要么比所有元素都小。

    • 当小于所有元素时,新元素放在底层数组的最开头
    • 当大于所有元素时,新元素放在底层数据的最末尾

升级操作的好处

    • 提升整数的灵活性,可以任意的向集合中放入3中不同类型的整数,而不用担心类型错误。
    • 节约内存,整数集合中只有大类型出现的时候才会进行升级操作。

整数集合不支持降级操作

 

7、压缩列表

压缩列表(ziplist)是Redis为了节约内存而开发,是一系列特殊编码的连续内存块组成的顺序型数据结构。

一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

下图为压缩列表的结构

每个压缩列表含有若干个节点,而每个节点都由三部分构成,previous_entry_length、encoding、content,如图:

 

    • previous_entry_length 存储的是前一个节点的长度,由于压缩列表内存块连续,使用此属性值可以计算前一个节点的地址,压缩列表就是使用这一原理进行遍历。
    • previous_entry_length 如果前一节点长度小于254字节,那么previous_entry_length属性本身长度为1字节,存储的指就是前一节点的长度;如果大于254个字节,那么previous_entry_length属性本身长度为5个字节,前一个字节为0xFE(十进制254),之后四个字节存储前一节点的长度。
    • encoding 记录本节点的content属性所保存数据的类型及长度,其本身长度为一字节、两字节或五字节,值得最高位为00、01或10的是字节数组的编码,最高位以11开头的是整数编码。
    • content 保存节点的值,可以是一个字节数组或者整数。

连锁更新

当对压缩列表进行添加节点或删除节点时有可能会引发连锁更新,由于每个节点的 previous_entry_length 存在两种长度1字节或5字节,当所有节点previous_entry_length都为1个字节时,有新节点的长度大于254个字节,那么新的节点的后一个节点的previous_entry_length原来为1个字节,无法保存新节点的长度,这是就需要进行空间扩展previous_entry_length属性由原来的1个字节增加4个字节变为5个字节,如果增加后原节点的长度超过了254个字节则后续节点也要空间扩展,以此类推,最极端的情况是一直扩展到最后一个节点完成;这种现象称为连锁更新。在日常应用中全部连锁更新的情况属于非常极端的,不常出现。

 

8、总结

Redis的底层数据结构共有六种,简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表。

Redis中的五大数据类型的底层就是由他们中的一种或几种实现,数据的存储结构最终也会落到他们上。

可是在redis命令下使用 OBJECT ENCODING 命令查看键值对象的编码方式,也就是是以哪种结构进行的底层编码。

Guess you like

Origin www.cnblogs.com/zh718594493/p/12099131.html