前言
在Redis中有许多数据结构,比如:简单动态字符串(SDS),双端链表,字典,压缩列表,整数集合等。
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统中包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象这物种类型的对象。通过这五种不同类型的对象,Redis可以在执行命令前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处时,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
除此之外,Redis对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。
最后,Redis对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时间较长的那些键可能会优先被服务器删除。
一. 对象的类型与编码
Redis使用对象来表示数据库的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
Redis中的每一个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性,encoding属性和ptr属性:
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;
1.1 类型
对象type属性记录了对象的类型,这个属性的值可以是下图中的一个。
对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象,列表对象,哈希对象,集合对象或有序集合对象的其中一种。
- 当我们称呼一个数据库键为"字符串键"时,我们指的是"这个数据库键所对应的值为字符串对象"
- 当我们称呼一个数据库键为"列表键"时,我们指的是"这个数据库键所对应的值为列表对象"
TYPE命令的实现方式也与此类似,当我们对一个数据库执行TYPE命令时,命令返回的结果为数据库键对应的值对象的类型,而不是键对象的类型。
下图列出了TYPE命令在面对不同类型的值对象时所产生的输出:
1.2 编码和底层实现
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象encoding属性决定。
encoding属性记录了对象所使用的编码,也就是说这个对象使用了什么数据结构作为对象的底层实现。
每种类型的对象都至少使用两种不同的编码,下图列出了每种类型的对象可以使用的编码:
使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码:
下图列出了不同编码的对象所对应的OBJECT ENCODING 命令输出:
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大的提升了Redis的灵活性和效率,因为Redis可以通过不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。
举个例子:在列表包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:
- 因为压缩列表比双端链表更节省内存,并且元素比较少时,在内存中以连续块方式保存压缩列表比双端链表可以更快被加载到内存中。
- 随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强,也更加适合保存大量元素的双端链表上面。
其他类型得得对象也通过使用多种不同的编码来进行类似的优化。
下面介绍Redis中的五种不同类型的对象,说明这些对象底层所使用的编码方式,列出对象从一种编码转化成另外一种编码所需的条件,以及同一个命令在不同编码上的实现方式。
二. 字符串对象
字符串对象的编码可以是int,raw或者emstr。
如果一个字符串对象保存的是整数值,并且这个整数值可以使用long类型来表示,那么字符串字符串对象会将整数值保存到字符串对象的ptr属性里(将void*转换成long),并将字符串对象的编码设置成int。
如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于32字节,那么字符串对象将使用一个简单的动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符对象将使用embstr编码的方式来保存这个字符串值。
2.1 embstr编码方式
embstr编码是专门用来保存短字符串的一种优化编码方式。这种编码方式和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象。但是raw编码会调用两次内存分配来分别创建redisObject结构和sdshdr结构。而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间依次包含redisObject和sdshdr两个结构。
embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果相同,但使用embstr编码的字符串对象来保存短字符串值有以下好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。
- 释放embstr编码字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。
- 因为embstr编码的字符串对象所有数据都保存在一块连续的内存里,所以这种编码的字符串比起raw编码的字符串对象能够更好地利用缓存带来地优势。
最后,可以用long double类型表示地浮点数在Redis中也是作为字符串值来保存地。如果我们要保存一个浮点数到字符串对象里面,那么程序会先将这个浮点数转换成字符串值,然后再保存转换所得地字符串值。
举个例子:执行以下代码将创建一个包含3.14的字符串表示"3.14"的字符串对象,在有需要的时候,程序会将保存的字符串对象里面的字符串值转换回浮点数值,执行某些操作,然后再将执行操作所得的浮点数转换回字符串值,并继续保存子字符串对象里。
下图总结并列出字符串对象保存各种不同类型的值所使用的编码方式:
2.2 编码转换
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码从int变为raw。
在下面的示例中,我们通过APPEND命令,向一个保存整数值的字符串对象追加了一个字符串值,因为追加操作只能对字符串值执行,所以程序会先将之前保存的整数值10086转换为字符串值"10086",然后再执行追加操作,操作的执行结果就是一个raw编码的,保存了字符串值的字符串对象:
因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有这些程序)。所以embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr编码转换成raw编码,然后再执行修改命令。 因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象。
2.3 字符串命令的实现
因为字符串键的值为字符串对象,所以用于字符串键的所有命令都是针对字符串对象来构建的,下图列举了其中一部分字符串命令,以及命令在不同编码的字符串对象下的实现方法:
三. 列表对象
在redis3.2版本之前的列表对象的编码可以是ziplist(压缩列表)或者linkedlist(双端链表)。
在redis3.2版本之后,已经不使用ziplist和linkedlist作为底层实现,取而代之的是quicklist。
3.1 redis3.2版本前的ziplist和linkedlist
ziplist编码的列表对象使用压缩列表作为底层实现,每一个压缩列表节点(entry)保存了一个列表元素。
举个例子:如果我们执行RPUSH命令,那么服务器创建一个列表对象作为number键的值:
127.0.0.1:6379> RPUSH NUMBERS 1 "THREE" 5
(integer) 3
如果numbers键的值对象使用的是ziplist编码,这个值对象将会是下图所展示的样子:
另一方面,linkedlist编码的列表对象使用双端链表作为底层实现,每一个双端链表的节点都保存了一个字符串对象,而每一个字符串对象都保存了一个列表元素。
举个例子:如果上面的列表使用的linkedlist编码,那么numbers键的值对象将是下图所示:
注意:
- linkedlist编码的列表对象在底层的双端链表结构中包含多个字符串对象,在之后介绍的哈希对象,集合对象和有序集合对象中都会出现,字符串对象是Redis五种类型的对象中唯一一个会被其他四种类型对象嵌套的对象。
- 为了简化字符串对象,我们在途中使用的是StringObject字样的格子表示一个字符串对象,而实际保存的值,比如一个包含"three"字符串值的字符串对象,如下图:
3.1.1 编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素长度都小于64字节
- 列表对象保存的元素数量小于512个,不能满足这两个条件的列表对象需要使用linkedlist编码。
注意:以上两个条件的上限值是可以修改的,具体请看配置文件中关于list-max-ziplist-value选项和list-max-ziplist-entries选项的说明。
对于使用ziplist编码的列表来说,使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有列表元素都会被转移并保持到双端链表里面,对象的编码也会从ziplist变为linkedlist。
例子:
1. 列表对象因为保存长度太大的元素而进行编码转换的情况:
127.0.0.1:6379> rpush blah "hello" "world" "again"
(integer) 3
127.0.0.1:6379> object encoding blah
"ziplist"
127.0.0.1:6379> rpush blah "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"
(integer) 4
127.0.0.1:6379> object encoding blah
"liskedlist"
127.0.0.1:6379>
2. 因为保存元素数量过多而进行编码转换
127.0.0.1:6379> EVAL "for i=1, 512 do redis.call('RPUSH', KEYS[1], i)end" 1 "integers"
(nil)
127.0.0.1:6379> llen integers
(integer) 512
127.0.0.1:6379> object encoding integers
"ziplist"
127.0.0.1:6379> rpush integers 513
(integer) 513
127.0.0.1:6379> object encoding integers
"linkedlist"
127.0.0.1:6379>
3.1.2 列表命令的实现
3.2 redis3.2版本之后的quicklist
3.2.1 为什么引入quicklist
- ziplist优缺点
优点:存储在连续的内存上,不容易产生内存碎片,内存利用率高。
缺点:插入和删除操作需要频繁的申请和释放内存,同时会发生内存拷贝,数据量大时,内存拷贝开销大。
- linkedlist优缺点
优点:插入和删除节点复杂度低。
缺点:除了报存数据外还需要保存prev和next指针,内存利用率低。并且双向链表各个节点时单独的内存块,不连续,容易造成内存碎片。
基于上述问题,quicklist的设计是时间和空间的折中,quicklist结合了ziplist和linkedlist的优点,核心思想是使用ziplist的特性,并使用linkedlist结构来减少ziplist的长度。
3.2.2 quicklist内部结构
quicklist每一个节点都是一个quicklistNode对象,数据结构如下:
typedef struct quicklistNode {
//前一个节点
struct quicklistNode *prev;
//后一个节点
struct quicklistNode *next;
//当前指向的ziplist或者quicklistLZF
unsigned char *zl;
//当前ziplist占用字节
unsigned int sz; /* ziplist size in bytes */
//ziplist中存储元素个数,16字节(最大65535个)
unsigned int count : 16; /* count of items in ziplist */
//是否采用LZF压缩算法压缩节点 1:RAW 2: LZF
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
//存储结构
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//当前ziplist是否需要再次被压缩(如果前面被解压过则为true,表示需要被再次压缩)
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;
Redis使用quicklist来管理所有的quicklistNode,结构如下:
typedef struct quicklist {
//列表头节点
quicklistNode *head;
//列表尾节点
quicklistNode *tail;
//ziplist一共存储了多少元素,即所有quicklistNode节点count和
unsigned long count; /* total count of all entries in all ziplists */
//双向链表的长度,即quicklistNode节点数
unsigned long len; /* number of quicklistNodes */
//填充因子
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
//压缩深度,0不压缩
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
quicklist结构实际是一个双向链表,链表中保存的元素是ziplist。这样当ziplist达到某一长度时,会新建一个quicklistNode节点。这样做的优点有:
- 使用到ziplist相比较于linkedlist,减少内存碎片。
- 使用到双向链表,减少单个ziplist的长度,减少内存拷贝的开销。
3.2.3 ziplist大小
虽然说quicklist结合了ziplist和双向链表的优点,但是现在的问题是ziplist大小的选择。
- ziplist太小,内存碎片越多
- ziplist太大,分配大块连续内存空间的难度越大,内存拷贝消耗越大。
如何保持ziplist的长度,取决于具体应用场景。
我们可以通过配置list-max-ziplist-size参数来控制ziplist的大小。基于两种原则,元素个数或元素大小。
- 当list-max-ziplist-size取正值的时候,表示按照数据项个数来限定每一个quicklist中ziplist的长度。比如:当list-max-ziplist-size等于5时,表示quciklist的ziplist节点最多包含5个数据项。
- 当list-max-ziplist-size取负值的时候,表示按照占用字节数来限定每一个quicklist中ziplist的长度。这时,它只能取-5到-1这五个值。表示的含义如下:
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
3.2.4 quicklist的compress属性
列表设计最容易访问的是列表两端的数据,中间的访问频率很低,如果符合这个场景,redis中还有一个配置,可以对列表的中将节点进行压缩(采用的LZF——无痕压缩算法),进一步节省内存。
list-compress-depth 0
compress属性表示压缩深度,这个属性表示quicklist两端不被压缩的节点数:
注意:这里压缩的节点是quicklist的节点quicklistNode,而不是ziplist里的数据。实际上压缩quicklist节点,包含的整个ziplist都会被压缩。
compress属性表示压缩深度可以通过配置list-compress-depth来控制:
- 0:不压缩(
默认值
) - 1:首尾第1个元素不压缩
- 2:首位前2个元素不压缩
- 3:首尾前3个元素不压缩
- 以此类推
3.2.5 quicklistNode的zl指针
zl指针默认指向ziplist,sz属性记录了当前ziplist占用的字节数。
但是当ziplist被压缩(LZF算法)后,zl指针指向另外一个对象quicklistLZF,quicklistLZF结构是一个4+N字节的结构。
typedef struct quicklistLZF {
//LZF的大小
unsigned int sz; /* LZF size in bytes*/
//被压缩的内容
char compressed[];
} quicklistLZF;
参考:【Redis】列表对象底层原理之quicklist_quicklist原理_云川之下的博客-CSDN博客