redis设计与实现读书笔记(二)

本文内容为《redis设计与实现》一书学习笔记。本文主要概述五到八章内容。

第5章 跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。
redis里有序表是跳表实现的 跳表不是搜索二叉树,它可以具备所有有序表规定的性能指标,也可以以logN水平改出所有的API,但它严格来讲是单链表。

跳表如果加了N个数据,那么0层的数据一定是N个,每个节点都有第0层,有多少数据拥有第1层?N/2,骰子以0.5概率生层,0.5概率停下,同理第2层数据N/4,第三层数据N/8……当高层数据很多的时候,高层前进一个,底层走了很多。每一个数据拥有几层和数据的值没关系,单纯扔骰子决定,所以高层索引每一层的索引和输入数据一点关系都没有,高层索引跳一个,底层索引跳非常多。这就是为什么加所有数据的时候从高层走起,利用高层索引的关系迅速找到这个数据要加的位置在哪。利用层数来做类似二分。
无论数据量N大小,跳表有多少层和N一点关系都没有,是完全解耦的,不管给的是什么数据,高层索引数量就那么多,一定是和数据量有关,而和具体key规律无关。N小的时候可能看不出来,但N数量起来之后,高层索引往低层索引先往右再往下的过程中,性能非常高效,就是logN水平。高层索引跳一步,在事实二分,不过这种二分不与key有关,而是与用概率roll出来的索引量有关,设计思想很先进。
当添加数据时,概念相当于从高层走若干步后到下一层卡了一个二叉树的路径,不走冤枉路,高层索引走了一步相当于底层索引走了很多步,类似二叉树卡了一个路径下来。

有关跳表的介绍,以及与平衡树、哈希表的比较和redis选用跳表而不是平衡树的原因可以参考此博客。

第6章 集合

当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为底层实现。



整数集合的每个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序排列,且数组中不含任何重复项。

升级
虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。 每当要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。升级步骤如下:

  • 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  • 将底层数组现在的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放到正确的位置上,放置时保持底层数组的有序性。
  • 将新元素添加到底层数组里(因为引发升级的新元素的长度总是比整数集合现有所有元素长度都大,所以这个新元素要么大于所有现有元素,要么小于所有现有元素;小于的情况放置位置为索引0,大于的情况放置位置为索引length-1)。

因为每次向整数集合添加新元素都可能引起升级,每次升级都需要对底层数组中已有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为O(N)。

升级的好处:

  • 提升整数集合的灵活性:可以随意地将int16_t、int32_t或者int64_t类型的整数添加到集合中,而不必担心出现类型错误;
  • 尽可能地节约内存:升级操作只会在有需要的时候进行;

降级
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。

第7章 压缩列表

压缩列表(ziplist)是列表和哈希的底层实现之一。当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现。(哈希的键值对的键和值具有这样的特征时,也会使用ziplist来实现)


压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。每个压缩列表节点可以保持一个字节数组或一个整数值。

时间复杂度分析:添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作。 连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N2)。 但这种操作出现的几率并不高,需要当前的内存分布情况恰好满足一定的条件时才可能触发。

第8章 对象

Redis并没有直接使用简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种如上的数据结构。
Redis通过引用计数技术,当程序不再使用某个对象时,这个对象所占用的内存就会被自动释放,还基于引用计数实现了对象共享机制,从而来实现节约内存的目的。
Redis的对象还记录了访问时间,这个信息可以用于计算数据库键的空转时间,空转时间较长的那些对象有可能被服务器删除掉。

8.1 对象类型和编码

Redis使用对象来表示数据库中的键和值,每当在Redis的数据库中新创建一个键值对时,至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。



键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。

编码决定了redis底层实现的数据结构。可以通过OBJECT ENCODING 命令来查看编码信息;Redis没有将一种类型关联到一种特定类型的编码上,极大的提升了Redis的灵活性和效率,Redis可以根据对象的特征进行不同的编码,比如,在一个列表键包含较少的列表项时,Redis就会使用压缩列表来实现,而当压缩列表不再适应的情况下,就会使用列表来实现。

8.2 字符串对象

字符串对象的编码可以是int、raw或者embstr。

SET number 10086 # int
SET story "Long, long ago there lived a king ..." # raw
SET msg "hello" # embstr

  1. 如果一个字符串保存的是整数值,那么编码就是int;
  2. 如果字符串对象保存的是一个字符串,并且字符串长度大于32字节,那么就会使用SDS来表示,编码为raw;
  3. 如果字符串对象保存的是一个长度小于等于32字节的字符串,那么就会使用embstr编码来保存;
  4. 如果保存的是用long double类型表示的浮点数,那么也是使用字符串的方式保存;

redis> SET pi 3.14
OK
redis> OBJECT ENCODING pi
"embstr"
redis> INCRBYFLOAT pi 2.0
"5.14"
redis> OBJECT ENCODING pi
"embstr"

embstr编码是专门用于保存短字符串的一种优化编码方式,embstr编码和raw的效果是一样的,但是embstr使用一次内存分配/回收来管理RedisObject结构和sdshdr结构,而raw会调用两次内存分配。并且embstr的redisObject和sdshdr是连续分布的,所以可以更好的利用缓存带来的优势。

int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。

redis> SET number 10086 # int
redis> APPEND number " is a good number!" # raw

embstr编码的字符串对象实际上是只读的。当对embstr编码的字符串对象执行任何修改命令时(比如APPEND),程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

8.3 列表对象

列表对象的编码可以是ziplist或者linkedlist。

当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:

  • 列表对象保存的所有字符串元素的长度都小于64字节;
  • 列表对象保存的元素数量小于512个; 不能满足这两个条件的列表对象需要使用linkedlist编码。当然,这两个限制在Redis配置文件中是可以修改的,可以查看配置文件中的list-max-ziplist-value和list-max-ziplist-entries。
    当条件不满足时,ziplist编码的对象会自动转换为linkedlist编码。

当执行以下RPUSH命令时,服务器将创建一个列表对象作为numbers键的值

redis> RPUSH numbers 1 "three" 5
(integer) 3


8.4 哈希对象

哈希对象的编码可以是ziplist或者hashtable。

ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后。

hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存。

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个;不能满足这两个条件的哈希对象需要使用hashtable编码。 限制条件可以在配置文件中修改(hash-max-ziplist-value,hash-max-ziplist-entries)。 当条件不满足时,ziplist编码的对象会自动转换为hashtable编码。

当执行以下HSET命令时,服务器将创建一个列表对象作为profile键的值

redis> HSET profile name "TOM"
(integer) 1
redis> HSET profile age 25
(integer) 1
redis> HSET profile career "Programmer"
(integer) 1

8.5 集合对象

集合对象的编码可以是intset或者hashtable。 intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。 hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。

当集合对象可以同时满足以下两个条件时,对象使用intset编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过512个。(可以配置set-max-intset-entries) 当条件不满足时,intset编码的对象会自动转换为hashtable编码。

当执行以下代码时,服务器将创建一个intset编码集合对象

redis> SADD numbers 1 3 5
(integer) 3

当执行以下代码时,服务器将创建一个hashtable编码集合对象

redis> SADD Dfruits "apple" "banana" "cherry"
(integer) 3

8.6 有序集合列表

有序集合的编码可以是ziplist或者skiplist。

ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序。

当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:

  • 有序集合保存的元素数量小于128个; # zset-max-ziplist-entries
  • 有序集合保存的所有元素成员的长度都小于64字节; # zset-max-ziplist-value

当条件不满足时,ziplist编码的对象会自动转换为skiplist编码。

当执行以下命令时,服务器将创建一个有序集合对象作为price键的值

redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:


为什么有序集合需要同时使用跳跃表和字典实现?


为了展示方便,上图在字典和跳跃表中重复展示了各个元素的成员和分值,但实际上字典和跳跃表会使用指针来共享元素的成员和分值,所以不会造成数据重复和内存浪费。

8.8 内存回收

Redis在自己的对象系统中构建了一个引用计数(redisObject结构的refcount属性)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。


8.9 对象共享

对象的引用计数属性还带有对象共享的作用。在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:

  • 将数据库键的值指针指向一个现有的值对象;
  • 将被共享的值对象的引用计数增一。

目前,Redis会在初始化服务器时创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时(包括嵌套对象的引用),服务器就会使用这些共享对象,而不是新创建对象。



这些共享对象不仅仅只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象(hashtable编码的哈希对象等)都可以使用这些共享对象。

查看引用计数:

redis> SET A 100
OK
redis> OBJECT REFCOUNT A
(integer) 2
redis> SET B 100
OK
redis> OBJECT REFCOUNT A
(integer) 3
redis> OBJECT REFCOUNT B
(integer) 3

尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享。

8.10 对象的空转时长

redisObject结构的lru属性记录了对象最后一次被命令程序访问的时间。
OBJECT IDLETIME命令可以打印出给定键的空转时长,其实就是将当前时间减去对象的这个时间戳。注意这个命令在访问键的值对象时,不会修改值对象的lru属性。
如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。

参考:

  1. 《redis设计与实现》
  2. 笔记1
  3. 笔记2

你可能感兴趣的:(redis设计与实现读书笔记(二))