第二章 简单动态字符串
Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串( simple dynamic string, SDS) 的抽象类型,并将SDS用作Redis的默认字符串表示。
在Redis里面,C字符串只会作为字符串字面量( string literal) 用在一些无须对字符串值进行修改的地方,比如打印日志
当Redis需要的不仅仅是一一个字符串字面量,而是一个可以被修改的字符串值时,Redis就会使用SDS来表示字符串值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。
Redis 只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS ( SimpleDynamic String,简单动态字符串)作为字符串表示。
比起C字符串,SDS具有以下优点:
1 )常数复杂度获取字符串长度。
2)杜绝缓冲区溢出。
3)减少修改字符串长度时所需的内存重分配次数。
4)二进制安全。
5)兼容部分C字符串函数。
第三章 链表 35
链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。 当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
链表中的每个节点都保存了一个整数值。
除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis 服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区( output buffer)
链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。
每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。
每个链表使用一个list结构来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
因为链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
通过为链表设置不同的类型特定函数,Redis 的链表可以用于保存各种不同类型的值。
第四章 字典 39
字典,又称为符号表( symbol table )、关联数组( associative array)或映射( map),是一种用于保存键值对( key-value pair )的抽象数据结构。在字典中,一个键(key)可以和一个值( value)进行关联( 或者说将键映射为值),这些关联的键和值就称为键值对。
字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等。
字典经常作为一种数据结构内置在很多高级编程语言里面,但Redis所使用的C语言并没有内置这种数据结构,因此Redis构建了自己的字典实现。
字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现
根据BGSAVE命令或BGREWRITEAOF命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write )技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写人操作,最大限度地节约内存。
字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis 使用MurmurHash2算法来计算键的哈希值。
哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表。
在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的。
第五章 跳跃表 54
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员( member )是比较长的字符串时,Redis 就会使用跳跃表来作为有序集合键的底层实现。
跳跃表是有序集合的底层实现之-一。
Redis 的跳跃表实现由zskiplist和zskiplistNode两个结构组成,中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
每个跳跃表节点的层高都是1至32之间的随机数。
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
第六章 整数集合 62
整数集合( intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
整数集合是集合键的底层实现之一。
整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型。
升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存。
整数集合只支持升级操作,不支持降级操作。
第七章 压缩列表 68
压缩列表( ziplist )是列表键和哈希键的底层实现之-一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型( sequential )数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表是一种为节约内存而开发的顺序型数据结构。
压缩列表被用作列表键和哈希键的底层实现之- -。
压缩列表可以包含多个节点,每个节点可以保存-一个字节数组或者整数值。
添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。
第八章 对象 76
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。
通过这五种不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
除此之外,Redis 的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。
最后,Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。
8.3 列表对象 84
举个例子,在列表对象包含的元素比较少时,Redis 使用压缩列表作为列表对象的底层实现:
因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载人到缓存中;
随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面;
完整的字符串对象显示
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
列表对象保存的所有字符串元素的长度都小于64字节;
列表对象保存的元素数量小于512个;
不能满足这两个条件的列表对象需要使用linkedlist编码。其中上限值可以在配置文件中修改
8.4 哈希对象 87
哈希对象的编码可以是ziplist或者hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推人到压缩列表表尾,然后再将保存了值的压缩列表节点推人到压缩列表表尾,因此:
保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
字典的每个键都是一个字符串对象,对象中保存了键值对的键;
字典的每个值都是一个字符串对象,对象中保存了键值对的值。
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
哈希对象保存的键值对数量小于512个;
不能满足这两个条件的哈希对象需要使用hashtable编码。
8.5 集合对象 91
集合对象的编码可以是intset或者hashtable。,
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
集合对象保存的所有元素都是整数值;
集合对象保存的元素数量不超过512个。
不能满足这两个条件的集合对象需要使用hashtable编码。
8.6 有序集合对象 93
有序集合的编码可以是ziplist或者skiplist。
ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员( member ),而第二个元素
则保存元素的分值( score )。
压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset {
zskiplist *zsl;
//第一部分数据结构与对象
dict *dict;
} zset;
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。
除此之外,zset 结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用0(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这-特性实现的,而很多其他有序集合命令都在实现的内部用到了这--特性。
有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一 个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存。
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
有序集合保存的元素数量小于128个;
有序集合保存的所有元素成员的长度都小于64字节;
不能满足以上两个条件的有序集合对象将使用skiplist编码。
8.7 类型检查与命令多态
Redis中用于操作键的命令基本上可以分为两种类型。
其中一种命令可以对任何类型的键执行,比如说DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。
而另一种命令只能对特定类型的键执行,比如说:
SET、GET、APPEND、STRLEN 等命令只能对字符串键执行;
HDEL、HSET、HGET、HLEN等命令只能对哈希键执行;
RPUSH、LPOP、LINSERT、 LLEN等命令只能对列表键执行;
SADD、SPOP、SINTER、 SCARD等命令只能对集合键执行;
ZADD、ZCARD、ZRANK、ZSCORE等命令只能对有序集合键执行;
8.8 内存回收
因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数( reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
对象的引用计数信息会随着对象的使用状态而不断变化:
在创建一个新对象时,引用计数的值会被初始化为1;
当对象被一个新程序使用时,它的引用计数值会被增一;
当对象不再被一个程序使用时,它的引用计数值会被减- -;
当对象的引用计数值变为0时,对象所占用的内存会被释放。
对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。
8.9 对象共享
在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:
将数据库键的值指针指向一个现有的值对象;
将被共享的值对象的引用计数增一。
目前来说,Redis会在初始化服务器时,创建一万个字符串对象, 这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
另外,这些共享对象不单单只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象( linkedlist编码的列表对象、hashtable 编码的哈希对象、hashtable 编
码的集合对象,以及zset编码的有序集合对象)都可以使用这些共享对象。
8.10 对象空转时常
除了前面介绍过的type、encoding、 ptr和refcount四个属性之外,redisobject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间:
OBJECT IDLETIME命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的1ru时间计算得出的,这个命令在访问键的值对象时,不会修改值对象lru属性
除了可以被OBJECT IDLETIME命令打印出来之外,键的空转时长还有另外一项作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru
或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
Redis数据库中的每个键值对的键和值都是一个对象。
Redis 共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。
Redis 的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
Redis 会共享值为0到9999的字符串对象。对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。