redis数据结构-底层编码+跳表详解

Posted Zheng"Rui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis数据结构-底层编码+跳表详解相关的知识,希望对你有一定的参考价值。

底层编码

节省空间

redis是将数据存储在内存中的,这样才使得redis能够非常高效,但是内存空间毕竟是珍贵的,如何能够节省内存空间也非常重要。

优化编码

Redis为每种数据类型都提供了两种数据编码方式,以散列类型为例,散列类型是根据散列表来实现的,这样可以实现O(1)时间复杂度的查找,插入,和删除工作,但是,当散列类型存储的数据量很小的时候,O(1)的时间复杂度并不会比O(n)的时间复杂度快很多,所以此时会采用一种在结构上更加紧凑,但是性能稍微低一点的数据编码,来实现散列表。
数据类型的编码方式对于开发者来说是透明的,也就是说,我们无法感知到内部数据编码的变化,redis会自动根据数据量的大小来调整编码方式。
我们可以通过object encoding命令来查看某个键的编码类型。
在Redis中每个键值都是由RedisObject结构体保存的,RedisObject结构体的定义如下:

typedef struct redisObject 
	unsigned type:4;
	unsigned notused:2; /* Not used */
	unsigned encoding:4;
	unsigned lru:22; /* lru time (relative to server.lruclock)/
	int refcount;
	void *ptr;
robj;

其中type表示该键的类型,主要取值有字符串类型,列表类型,散列类型,集合类型和有序集合类型。ptr是用来指向真实数据存放的地址。
encoding用来表示该键的编码方式,主要有一下取值:

#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 ed as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */

下面介绍每种数据类型可能会有的编码方式

1.1 字符串类型

1.1.1 sdshdr

redis使用sdshdr类型的变量来存储字符串,而之前提到的RedisObject中的ptr就指向这个变量的地址。
sdshdr的结构如下:

struct sdshdr 
	int len;
	int free;
	char buf[];
;

其中len用来表示字符串的长度,free表示buf中的剩余空间,而buf中存储的才是真实的字符数据(这里可以猜到为什么redis中的字符串是安全的,因为它在存储字符串的时候,存储了字符串的长度,不像c语言中使用’\\0’为结尾,而是根据长度进行判断)。
综上,一个字符类型的键占据的大小是RedisObject的大小 + sdshdr的大小 + buf的大小,三个部分组成。

1.1.2 long类型

当字符串存储的数据可以转化成一个64位整数的时候,redis会自动将数据编码转化成long类型,此时占用的空间就只有RedisObject的空间里,可以节省差不多一半的内存。
在前面介绍的RedisObject结构体中,refcount字段表示的是当前RedisObject被几个键所引用,也就是说在redis中RedisObject是可以共享的。在redis启动的时候,会预先建立10000个分别存储从0到9999这些数字的RedisObject,当之后需要建立在这个数字范围内的键的时候,只需要引用即可,不会新创建RedisObject。由此可见,在redis中创建小数字值的键的代价是很低的,只需要增加引用就可以了。

1.2 散列类型

在散列类型中可能的编码方式有REDIS_ENCODING_HT或者REDIS_ENCODING_ZIPLIST
这两种编码方式的转化时机可以在redis的配置文件中,通过hash-max-ziplist-entrieshash-max-ziplist-value,当散列类型的字段个数少于hash-max-ziplist-entries并且字段名和字段值的长度都小于hash-max-ziplist-value的时候,就会使用ziplist编码方式进行存储,否则会使用散列表。这个转化过程是透明的,每当散列类型的键值发生改变的时候,都会去判断此时有没有满足转化条件。

1.2.1 HashTable

REDIS_ENCODING_HT也就是散列表,可以实现O(1)时间复杂度内的数据查找,更新,删除,其中的字段名和字段值都是使用RedisObject来存储的,也就是字符串类型,所以上文描述的对字符串类型的优化,在此场景下也生效。
但是,我们知道Redis中的键值也是通过散列方式存储的,但是Redis的键名并不是使用RedisObject来存储的,也就用不了那些数值优化,所以将redis中的键名设置成纯数字是不会有性能提升的。当然,在大多数情况下,键名也不建议设置为纯数字。

1.2.2 ZIPLIST

REDIS_ENCODING_ZIPLIST叫做压缩表。它是一种比较紧凑的数据格式,通过牺牲部分的性能,还换取极高的空间利用率,适合在元素较少的适合使用。该编码类型同样在列表类型和有序集合类型中使用。
压缩表的数据结构大致如下图所示:

其中zlbytes是一个uint32的整数,用来表示整个结构体占用的空间。zltail也是一个uint32类型的整数,用来表示最后一个元素的偏移。通过zltail可以使得程序可以直接定位结构体末尾而无须遍历整个结构,在进行尾插尾删的时候很有用。zllen用来记录结构体中存放的元素数量。zlend用来标识结构体的结束。
在压缩表中,每一个元素都由四个部分组成。
第一个部分用来存储前一个元素的大小,这样能够方便进行倒序查找。
二三部分纪录了当前元素的类型和大小。
第四个部分记录的是元素的实际数据内容,如果元素可以转化成数字的话,redis会使用相应的数字类型来进行存储,以节省空间。
当使用压缩表来编码散列类型的时候,它的做法是,在元素存放的顺序固定为 字段名 字段值 字段名 字段值。。。。。以此类推。
当我们要在压缩表编码的散列类型键中查找某一元素的时候,首先从第一个元素查起,每次跳过一个元素(这样能保证每次查的都是字段名),并且在插入和删除的时候,都需要移动后面的所有元素。所以可想而知,当元素过多时,它的效率会有严重降低。所以不适合存储大量数据。

1.3 列表类型

列表类型的编码方式可能是REDIS_ENCODING_ZIPLIST或者REDIS_ENCODING_LINKEDLIST其转化时机有配置文件中的
list-max-ziplist-entrieslist-max-ziplist-values来决定,具体方式和散列表相同。这里不在赘述。

1.3.1 LINKEDLIST

REDIS_ENCODING_LINKEDLIST也就是双向链表。链表中的每个元素都是由RedisObject组成的,这种方式下的优化方法和字符串的优化方法相同。

1.3.2 ZIPLIST

列表在元素较少时也会使用ziplist,因为ziplist支持倒序查找,所以就算从后往前查找效率也比较高。

1.4 set集合类型

集合类型的编码方式可能是REDIS_ENCODING_HT或者是REDIS_ENCODING_INTSET这两种数据类型。当集合中的所有元素都是整数并且字段个数小于set-max-intset-entries中配置的最大值时,会使用INTSET,否则会使用散列表。

1.4.1 intset整数集合

intset结构体类型如下所示:

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

其中encoding用来表示每个集合中每个元素占用的字节数,分为int16,int32,int64等等。length用来表示集合中存储的元素个数。contents用来存储所有的整数数据。
intset编码以有序的方式存储元素,所有此时使用smembers来获取所有元素的顺序是一致的。可以使用二分查找来查找元素。但是,进行元素插入和删除的时候,都需要移动后面的元素,当元素过多时,效率不高。
所以当集合中元素个数超过set-max-intset-entries的时候,就会使用散列表进行编码。
需要注意的是:当集合的编码方式变成散列表之后,就算之后删除了大量元素也不会变回intset,因为如果要变,就需要在每次操作完一个字段之后,检查所有字段,当前是否满足条件,那么时间复杂度就不是O(1)了。

1.5 zset有序集合

在有序集合中的编码方式是REDIS_ENCODING_ZIPLISTREDIS_ENCODING_SKIPLIST也就是压缩表和跳表。它的转化时机同样由配置文件中的zset-max-ziplist-entrieszset-max-ziplist-values来决定的。
redis使用压缩表来存储元素值和对应分数的映射,也就是在元素存储的时候,通过元素值 分数 元素值 分数。。。这样的顺序进行存储的,并且分数是按序存储的。
当zset中的数据量达到阈值之后,redis就会自动使用SKIPLIST来实现有序集合。

1.5.1 SKIPLIST跳表

redis会在数据量较多时,使用跳表是来存储元素值和分数的映射来实现排序功能。
在redis的源码实现中,SKIPLIST的每一个结点都是通过zskiplistnode结构体来保存信息的,而skiplist结构体用来保存结点信息,比如结点的数量,指向跳表头结点和尾结点的指针。

上图展示了一个跳表实现zset的例子,其中head用来指向头结点,tail用来指向尾结点,level表示的是当前跳表内最高的结点的层数,length用来表示当前跳表内的元素个数(不包括头结点)。
而跳表中head指针连接的就是跳表结点,其中每一个跳表结点都包含了一下信息:
1.后退指针,用来指向当前结点的前一个结点,用于反向遍历
2.分值(socre),跳表根据分值进行排序
3.对象:每个结点中保存的数据成员,对应图中的o1,o2,o3
4.前进指针:后来指向当前层的表尾方向的下一个结点
5.跨度:用来表示前进指针走到下一个结点的距离
注意表头结点的结构和其他结点是一样的,也有后退结点,分值,对象等等,但是这些信息都不会被用到。
下面是zsikplist结构体的代码表示:

typedef struct zskiplistNode 
//层
struct zskiplistLevel 
	//前进指针
	struct zskiplistNode *forward;
	//跨度
	unsigned int span;
 level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
 zskiplistNode;

下面进行详细介绍

1.5.1.1 层

跳跃表结点的level数组可以包含很多元素,每一个元素都包含了一个指向下一个元素的前进指针,程序可以通过这些层来加快对结点的访问,一般来说,层数越多,访问结点的速度就越快。
每次创建一个新的跳表结点的时候,都会通过一定的算法,生成当前的层数(保证每一个结点的层数都是均匀分布的,这是一个概率问题,又可能会出现最高的几个结点都在一起的情况),这里的层数就是level数组的长度。

1.5.1.2 前进指针

如上文所述,每一层都包含一个指向表尾方向的前进指针。用于从表头方向向表尾进行遍历。当遇到下一个结点为空,或者下一个结点的值大于要找的值的时候,进行指针下沉,也就是到下一层中,继续进行遍历。

1.5.1.3 跨度span

每一层不仅包含了前进指针,还包含了通过这个前进指针走向下一个结点,实际上通过的距离。
这个距离的目的不在于方便遍历,因为遍历的话,只需要前进指针就够了。这里其实是为了实现rank排序,在查找某个值的排位的时候,只需要将经过路径上的所有跨度相加即可。

1.5.1.4 后退指针

后退指针用于从表尾向表头方向访问数据,但是与前进指针不同的是,后退指针是在结点中的,不在层中,每个结点只有一个后退指针,所以通过后退指针,每次只能后退到前一个结点。不可以想前进指针一下跳过多个结点。

1.5.1.5 分值与成员

分值score是一个double类型的浮点数,zset就是通过分值来进行排序的。
对象成员其实在跳表中存储的是对象的指针,用来指向对象地址。
在同一个跳跃表中,对象是唯一的,但是不同的对象可以有相同的分数,跳表会先使用分值进行排序,分值相同的,会根据对象的字典序进行排序。

1.5.1.6 跳表结构

下面介绍跳表结构体。
依靠上文所述的跳表结点就可以组建一张跳表。跳表结构体定义如下:

typedef struct zskiplist 
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
 zskiplist;

通过head和tail两个指针,可以在o(1)的时间复杂度内访问跳表的头尾指针。通过length可以在o(1)的时间复杂度内获取跳表长度。level用来获取跳表中最高一层结点的层高。

以上是关于redis数据结构-底层编码+跳表详解的主要内容,如果未能解决你的问题,请参考以下文章

Redis底层数据结构详解

Redis底层数据结构详解

Redis底层数据结构详解

Redis底层数据结构详解

Redis必知必会之zset底层—Skip List跳跃列表(面试加分项)

Redis - zset底层数据结构实现