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使用压缩表来存储元素值和对应分数的映射,也就是在元素存储的时候,通过元素值 分数 元素值 分数。。。这样的顺序进行存储的,并且分数是按序存储的。
而跳表是用来存储元素值和分数的映射来实现排序功能。

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

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

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

redis 底层数据结构 压缩列表 ziplist

Redis中hash、set、zset的底层数据结构原理

Redis_17_Redis服务器中的数据库(五种基本类型底层存放)

Redis_17_Redis服务器中的数据库(五种基本类型底层存放)