Part I 数据结构与对象
2. 简单动态字符串
Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
-
Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS作为字符串表示。
-
当Redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis就会使用SDS来表示字符串值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。
-
比起C字符串,SDS有以下优点:
-
常数复杂度O(1) 获取字符串长度,因为使用len属性来保存长度
-
杜绝缓冲区溢出。
-
减少修改字符串长度时所需的内存重分配次数,实现了空间预分配和惰性空间释放
-
二进制安全。
-
3. 链表
-
链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。
-
链表节点由一个listNode结构来表示,Redis链表是双端链表。每个链表使用一个list结构来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
-
因为链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。
-
通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。
4. 字典
-
Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
-
Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
-
哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表。
-
在对哈希表进行扩展或者收缩操作时,需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是分多次、渐进式地完成的。
5. 跳跃表
-
Redis只在两个地方用到了跳跃表,一个是实现有序集合键(Zset),另一个是在集群节点中用作内部数据结构
-
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找
-
由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息,而zskiplistNode则用于表示跳跃表节点。
-
每个跳跃表节点的层高都是1至32之间的随机数。
-
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
6. 整数集合
-
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
-
整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素。会根据新添加元素的类型,改变这个数组的类型。
-
升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存。整数集合只支持升级操作,不支持降级操作。
7. 压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
-
压缩列表是一种为节约内存而开发的顺序型数据结构。压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
-
添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。
8. 对象
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,
Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制。
8.1 对象的类型与编码
Redis使用对象来表示数据库中的键和值,在Redis的数据库中新创建一个键值对时,至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
-
Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性(指向底层实现数据结构的指针)
-
键总是一个字符串对象,而值可以是任何对象。使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码
-
在列表对象包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:
-
因为压缩列表比双端链表更节约内存,并且在元素数量较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到缓存中
-
随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转向功能更强、也更适合保存大量元素的双端链表上面;
-
8.2 字符串对象
字符串对象的编码可以是int、raw或者embstr。
-
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。
-
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。
-
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
-
embstr编码是专门用于保存短字符串的一种优化编码方式,在执行修改命令之后,会变成一个raw编码的字符串对象。
命令:
SET、GET、APPEND、INCRYFLOAT、INCRBY、DECRBY、STREN、SETRANGE、GETRANGE
8.3 列表对象
列表对象的编码可以是ziplist或者linkedlist。
-
ziplist编码的列表对象使用压缩列表作底层实现,每个压缩列表节点(entry)保存了一个列表元素。
-
linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
-
当列表对象的所有字符串元素的长度都小于64字节,元素数量小于512个时,列表对象使用ziplist编码,否则使用linkedlist编码。
命令:
LPUSH、RPUSH、LPOP、RPOP、LINDEX、LLEN、LINSERT、LTRIM、LSET
8.4 哈希对象
哈希对象的编码可以是ziplist或者hashtable。
-
ziplist编码的哈希对象使用压缩列表作为底层实现,hashtable编码的哈希对象使用字典作为底层实现
-
字符串长度小于64字节,或者键值对数量小于512个使用压缩列表。
命令:
HSET、HGET、HEXISTS、HDEL、HLEN、HGETALL
8.5 集合对象
集合对象的编码可以是intset或者hashtable。
-
intset编码的集合对象使用整数集合作为底层实现,hashtable编码的集合对象使用字典作为底层实现,字典的每个键包含了一个集合元素,而字典的值则全部被设置为NULL。
-
对象使用intset编码:集合对象保存的所有元素都是整数值,以及集合对象保存的元素数量不超过512个。
命令:
SADD、SCARD、SISMEMBER、SMEMBERS、SRANDMEMBER、SPOP、SREM
8.6 有序集合对象
有序集合的编码可以是ziplist或者skiplist。
-
使用压缩列表时,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。
-
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
-
zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:
-
跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。
-
通过跳跃表,可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令。
-
-
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典的键保存了元素的成员,而字典的值则保存了元素的分值。
-
通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现
-
为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
-
-
当有序集合保存的元素数量小于128个,以及长度都小于64字节时不使用跳表,而使用压缩列表。
命令:
ZADD、ZCARD、ZCOUNT、ZRANGE、ZREVRANGE、ZRANK、ZREVRANK、ZREM、ZSCORE
8.7 内存回收和对象共享
-
Redis构建了一个引用计数技术实现的内存回收机制,对象的引用计数信息由redisObject结构的refcount属性记录,初始化为1。
-
当对象被一个新程序使用时,它的引用计数值会被增一;
-
当对象不再被一个程序使用时,它的引用计数值会被減一;
-
当对象的引用计数值变为0时,对象所占用的内存会被释放。
-
-
对象的引用计数属性还带有对象共享的作用。
-
Redis会在初始化时创建包含了从0到9999的所有整数值的对象,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
-
Redis只对包含整数值的字符串对象进行共享。Redis不共享包含字符串的对象是因为验证复杂度较高。
-
-
对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。