前面介绍了Redis用到的全部主要数据结构,好比简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合等。然而Redis并无直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构建立了一个对象系统,包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种前面所介绍的数据结构。git
Redis在执行命令以前,根据对象的类型来判断一个对象是否能够执行给定的命令。还能够针对不一样的使用场景,为对象设置多种不一样的数据结构实现,从而优化对象在不一样场景下的内存使用效率。github
Redis的对象系统还实现了基于引用计数的内存回收机制,当某个对象的引用计数为0时,这个对象所占用的内存就会被自动释放;Redis还经过引用计数技术实现了对象共享机制,经过让多个键共享同一个对象来节约内存。redis
最后,Redis的对象带有访间时间记录信息,在服务器启用了maxmemory功能的状况下,长时间未使用的那些键可能会优先被服务器删除。算法
一:对象的类型和编码数据库
Redis使用对象来表示数据库中的键和值,每当在Redis的数据库中新建立一个键值对时,至少会建立两个对象,一个对象用做键,另外一个对象用做值。好比下面的命令:缓存
127.0.0.1:6379> set msg "hello world" OK
在数据库中建立了一个新的键值对,其中键是一个包含了字符串值"msg"的对象,而值则是一个包含了字符串值"hello world"的对象。服务器
对于Redis中的键值对来讲,键老是一个字符申对象,而值能够是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象。数据结构
Redis中的每一个对象都由一个redisObject结构表示,该结构在redis.h中定义:app
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; void *ptr; } robj;
type字段表示对象的类型,取值能够是以下内容之一:函数
#define REDIS_STRING 0 #define REDIS_LIST 1 #define REDIS_SET 2 #define REDIS_ZSET 3 #define REDIS_HASH 4
Redis中的TYPE命令能够返回对象的类型:
127.0.0.1:6379> type msg string
redisObject的ptr指针指向对象底层的数据结构,而数据结构的类型由redisObject的encoding字段决定。取值能够是以下内容之一:
#define REDIS_ENCODING_RAW 0 /* Raw representation */ #define REDIS_ENCODING_INT 1 /* Encoded 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 */ #define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
其中,字符串对象可使用的编码有:REDIS_ENCODING_RAW 、REDIS_ENCODING_INT和REDIS_ENCODING_EMBSTR,列表对象可使用的编码有:REDIS_ENCODING_LINKEDLIST和REDIS_ENCODING_ZIPLIST,哈希对象可使用的编码有:REDIS_ENCODING_HT、和REDIS_ENCODING_ZIPLIST,集合对象可使用的编码有:REDIS_ENCODING_HT和REDIS_ENCODING_INTSET,有序集合可使用的编码有:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST。
在Redis的2.6版本以前,包含少许键值对的哈希对象使用REDIS_ENCODING_ZIPMAP编码。自2.6开始,开始使用REDIS_ENCODING_ZIPLIST,做为包含少许键值对的哈希对象的实现。
使用DBJECT ENCDDING命令能够查看一个值的编码:
127.0.0.1:6379> object encoding msg "embstr"
对象在不一样的场景下,使用不一样的数据结构来实现,极大地提高了Redis的灵活性和效率。好比列表对象包含的元素比较少时,Redis使用压缩列表做为列表对象的底层实现,这样作更加节约内存,而且在内存中以连续块方式保存的压缩列表比起双端链表,能够更快被载人到缓存中。随着列表对象包含的元素愈来愈多,使用压缩列表来保存元素的优点逐渐消失时,对象就会将底层实现从压缩列表转向双端链表上面。
二:字符串对象
字符串对象的编码能够是int、raw或者embstr。
若是一个字符串对象保存的是整数值,而且这个整数值能够用long类型来表示,那字符串对象会将整数值保存在字符串对象redisObject结构的ptr属性里面,也就是将void*转换成long。内存布局以下:
好比:
127.0.0.1:6379> set number 123 OK 127.0.0.1:6379> object encoding number "int"
若是字符串对象保存的是一个字符串值,而且这个字符串值的长度大于39字节,那么字符串对象使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。内存布局以下,注意,对象的ptr指针,不是指向sdshdr的首地址,而是指向原始字符串的首地址:
若是字符串对象保存的是一个字符串值,而且这个字符串值的长度小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码同样,都使用redisObject结构和sdshdr结构来表示字符串对象。
但raw编码会调用两次内存分配函数来分别建立redisObject结构和sdshdr结构,而embstr编码则经过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr结构。这样作的好处是:内存的分配和释放更加高效,并且embstr编码的字符串对象,使用连续的内存块,可以更好地利用缓存带来的优点。其内存布局以下:
好比:
127.0.0.1:6379> set msg "abcdefghijklmnopqrstuvwxyz0123456789012" OK 127.0.0.1:6379> object encoding msg "embstr" 127.0.0.1:6379> set msg "abcdefghijklmnopqrstuvwxyz01234567890123" OK 127.0.0.1:6379> object encoding msg "raw"
int编码的字符串对象和embstr编码的字符申对象,在知足必定条件的状况下,会被转换为raw编码的字符串对象。
int编码的字符串对象,若是向对象执行了一些命令,使得这个对象保存的再也不是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw。好比下面的例子:
127.0.0.1:6379> set number 1 OK 127.0.0.1:6379> object encoding number "int" 127.0.0.1:6379> append number " is a number" (integer) 13 127.0.0.1:6379> object encoding number "raw"
Redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有这些程序),因此embstr编码的字符串对象其实是只读的。
当对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,而后再执行修改命令。由于这个缘由,embstr编码的字符串对象在执行修改命令以后,总会变成一个raw编码的字符串对象。好比:
127.0.0.1:6379> set msg "hello" OK 127.0.0.1:6379> object encoding msg "embstr" 127.0.0.1:6379> append msg " world" (integer) 11 127.0.0.1:6379> object encoding msg "raw"
三:列表对象
列表对象的编码能够是ziplist或者linkedlist。
当列表对象中的元素较少,且元素都是较短的字符串时,列表对象采用ziplist编码,也就是使用压缩列表(ziplist)做为底层实现,每一个压缩列表节点(entry)保存一个列表元素。
好比:
127.0.0.1:6379> rpush number 1 "three" 5 (integer) 3 127.0.0.1:6379> object encoding number "ziplist"
采用ziplist编码时,内存布局以下,在ziplist中,每一个节点中保存的是原始字符串(“three”)或者数字(123),而非redisObject结构,或者sdshdr结构:
当列表对象能够同时知足如下两个条件时,才会使用ziplist编码:
a:列表对象中,每一个元素保存的字符串元素的长度都小于64字节;
b:列表对象保存的元素数量小于512个。
当条件不知足时,列表对象使用linkedlist编码,也就是采用双端链表(list)做为底层实现,每一个链表节点(listNode)保存一个列表元素(字符串对象)。
列表对象采用linkedlist编码时,内存布局以下,在list链表中,每一个节点listNode结构中的value指针,指向一个redisObject结构:
linkedlist编码的列表对象,在底层的双端链表结构中包含了多个字符串对象,这种嵌套字符串对象的行为在稍后介绍的哈希对象、集合对象和有序集合对象中都会出现。字符串对象是Redis五种类型的对象中,惟一一种会被其余四种类型对象嵌套的对象。
对于使用ziplist编码的列表对象,当插入操做使得两个条件中的任意一个不知足时,就会执行编码转换。本来保存在压缩列表里的全部列表元素都会被转移并保存到双端链表中,对象的编码从ziplist变为linkedlist。
好比,下面分别演示了插入长度超过64字节的元素,以及元素数超过512的状况:
127.0.0.1:6379> rpush number "0123456789012345678901234567890123456789012345678901234567890123" (integer) 4 127.0.0.1:6379> object encoding number "ziplist" 127.0.0.1:6379> rpush number "01234567890123456789012345678901234567890123456789012345678901234" (integer) 5 127.0.0.1:6379> object encoding number "linkedlist" 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"
这两个条件中的阈值,能够经过配置文件中的如下选项进行修改:
list-max-ziplist-entries 512 list-max-ziplist-value 64
四:哈希对象
哈希对象的编码能够是:ziplist或者hashtable。
当哈希对象中的键值对较少,且键值对中保存的都是较短的字符串时,哈希对象采用zipiist编码,也就是使用压缩列表(ziplist)做为底层实现。当有新的键值对要加人到哈希对象时,会分别将键和值中的原始字符串添加到压缩列表的表尾。
所以,压缩列表中,保存了同一键值对的两个节点老是紧挨在一块儿,保存键的节点在前,保存值的节点在后。先添加到哈希对象中的键值对在前,后添加的键值对在后。
好比:
127.0.0.1:6379> hset profile name "tom" (integer) 1 127.0.0.1:6379> hset profile age 25 (integer) 1 127.0.0.1:6379> hset profile career "programmer" (integer) 1 127.0.0.1:6379> object encoding profile "ziplist"
哈希对象采用ziplist编码时,内存布局与ziplist编码的列表对象相似。在压缩列表中,相邻两个节点中保存的,分别是key和value的原始字符串,而非redisObject结构,或者sdshdr结构,好比下图,就表示上面例子中,哈希对象profile的内存结构:
当哈希对象能够同时知足如下两个条件时,才会使用ziplist编码:
a:哈希对象中,全部键值对中,保存的字符串元素的长度都小于等于64字节;
b:列表对象保存的键值对数量小于等于512个。
当条件之一不知足时,哈希对象使用hashtable编码,也就是采用字典(dict)做为底层实现,字典的哈希表中,每一个哈希节点(dictEntry)保存一个键值对(两个字符串对象)。
哈希对象采用hashtable编码时,内存布局以下,在dict字典的哈希表dictht中,每一个哈希表节点dictEntry结构中,key指针指向一个redisObject结构,表示键对象,v.val指针指向一个redisObject结构,表示值对象:
对于使用ziplist编码的哈希对象来讲,当插入操做使得两个条件中的任意一个不能被知足时,就会执行编码转换。本来保存在压缩列表里的全部键值对都会被转移并保存到字典中,对象的编码从ziplist变为hashtable。
好比,下面分别演示了插入长度超过64字节的键、值,以及键值对数目超过512的状况:
127.0.0.1:6379> hset testhash 0123012345678901234567890123456789012345678901234567890123456789 "hehe2" (integer) 1 127.0.0.1:6379> object encoding testhash "ziplist" 127.0.0.1:6379> hset testhash 01234012345678901234567890123456789012345678901234567890123456789 "hehe2" (integer) 1 127.0.0.1:6379> object encoding testhash "hashtable" 127.0.0.1:6379> hset testhash2 name "0123012345678901234567890123456789012345678901234567890123456789" (integer) 1 127.0.0.1:6379> object encoding testhash2 "ziplist" 127.0.0.1:6379> hset testhash2 name2 "01234012345678901234567890123456789012345678901234567890123456789" (integer) 1 127.0.0.1:6379> object encoding testhash2 "hashtable" 127.0.0.1:6379> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i) end" 1 "testhash3" (nil) 127.0.0.1:6379> hlen testhash3 (integer) 512 127.0.0.1:6379> object encoding testhash3 "ziplist" 127.0.0.1:6379> hset testhash3 name "tom" (integer) 1 127.0.0.1:6379> hlen testhash3 (integer) 513 127.0.0.1:6379> object encoding testhash3 "hashtable"
这两个条件中的阈值,能够经过配置文件中的如下选项进行修改:
hash-max-ziplist-entries 512 hash-max-ziplist-value 64
五:集合对象
集合对象的编码能够是intset或者hashtable。
当集合对象中的元素较少,且都是整数时,集合对象采用intset编码,也就是使用整数集合(intset)做为底层实现。好比:
127.0.0.1:6379> sadd numbers 1 2 3 (integer) 3 127.0.0.1:6379> object encoding numbers "intset"
集合对象采用intset编码时,内存布局以下,redisObject中的ptr指向一个整数集合,在整数集合的contents中直接保存整数,而非redisObject结构,或者sdshdr结构:
当集合对象能够同时知足如下两个条件时,才会使用intset编码:
a:集合对象保存的全部元素都是整数值;
b:集合对象保存的元素数量不超过512个。
当条件之一不知足时,集合对象使用hashtable编码,也就是采用字典(dict)做为底层实现,字典的哈希表中,每一个哈希节点(dictEntry)的key保存一个元素(字符串对象),并将哈希节点的value置为NULL。
集合对象采用hashtable编码时,内存布局以下,在dict字典的哈希表dictht中,每一个哈希表节点dictEntry结构中,key指针指向一个redisObject结构,表示集合元素,v.val直接置为NULL:
对于使用intset编码的集合对象来讲,当插入操做使得两个条件中的任意一个不能被知足时,就会执行编码转换。本来保存在整数集合里的全部元素都会被转移并保存到字典中,对象的编码从intset变为hashtable。
好比,下面分别演示了插入非整数的元素,以及元素数目超过512的状况:
127.0.0.1:6379> sadd numbers 1 2 3 (integer) 3 127.0.0.1:6379> object encoding numbers "intset" 127.0.0.1:6379> sadd numbers hehe (integer) 1 127.0.0.1:6379> object encoding numbers "hashtable" 127.0.0.1:6379> EVAL "for i=1, 512 do redis.call('SADD', KEYS[1], i) end" 1 integers (nil) 127.0.0.1:6379> scard integers (integer) 512 127.0.0.1:6379> object encoding integers "intset" 127.0.0.1:6379> sadd integers 1000 (integer) 1 127.0.0.1:6379> scard integers (integer) 513 127.0.0.1:6379> object encoding integers "hashtable"
条件中的阈值,能够经过配置文件中的如下选项进行修改:
set-max-intset-entries 512
五:有序集合对象
有序集合的编码能够是ziplist或者skiplist。
当集合对象中的元素较少,且元素中的字符串长度较小时,有序集合对象采用ziplist编码,也就是使用压缩列表(ziplist)做为底层实现。
每一个集合元素使用两个紧挨的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。
好比:
127.0.0.1:6379> zadd price 8.5 apple 5.0 banana 6.0 cherry (integer) 3 127.0.0.1:6379> object encoding price "ziplist"
有序集合对象采用ziplist编码时,内存布局与ziplist编码的列表对象相似。在压缩列表中,相邻两个节点中保存的,分别是元素成员和分值的原始字符串和double浮点数,而非redisObject结构,或者sdshdr结构,好比下图,就表示上例中,有序集合price的内存结构:
当有序集合对象能够同时知足如下两个条件时,才会使用ziplist编码:
a:有序集合保存的元素数量小于等于128个;
b:有序集合保存的全部元素中,原始字符串的长度都小于等于64字节。
当条件之一不知足时,有序集合对象使用skiplist编码,也就是采用zset结构做为底层实现,zset由跳跃表(skiplist)和字典(dict)两种数据结构组成:
typedef struct zset { dict *dict; zskiplist *zsl; } zset;
zset结构中的跳跃表zskiplist,按分值从小到大保存全部集合元素,每一个跳跃表节点zskiplistNode中,obj属性保存了元素的成员,而score属性则保存了元素的分值。经过这个跳跃表,程序能够对有序集合进行范围型操做,好比ZRANK,ZRANGE等命令就是基于跳跃表API来实现的。
除此以外,zset结构中的dict字典,为有序集合建立了一个从成员到分值的映射,字典中的每一个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。经过这个字典,程序能够用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的。
有序集合每一个元素的成员都是一个字符串对象,而每一个元素的分值都是一个double类型的浮点数。虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构会经过指针来共享相同元素的成员和分值,因此同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会所以而浪费额外的内存。
有序集合对象采用skiplist编码时,内存布局以下,在skiplist的节点zskiplistNode中,obj指针指向一个redisObject结构,表示有序集合元素的成员,在dict字典的哈希表dictht中,每一个哈希表节点dictEntry结构中,key指针指向一个同一个redisObject结构;zskiplistNode中的score记录有序集合元素的分数,在dict字典的哈希表dictht中,每一个哈希表节点dictEntry结构中,在v.val指向该zskiplistNode中的score:
对于使用ziplist编码的有序集合对象来讲,当插入操做使得两个条件中的任意一个不能被知足时,就会执行编码转换。本来保存在压缩列表里的全部元素都会被转移并保存到zset结构中,对象的编码从ziplist变为skiplist。
好比,下面分别演示了插入长度超过64字节的元素,以及键值对数目超过128的状况:
127.0.0.1:6379> zadd testzset 1.0 0123012345678901234567890123456789012345678901234567890123456789 (integer) 1 127.0.0.1:6379> object encoding testzset "ziplist" 127.0.0.1:6379> zadd testzset 2.0 01234012345678901234567890123456789012345678901234567890123456789 (integer) 1 127.0.0.1:6379> object encoding testzset "skiplist" 127.0.0.1:6379> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 testzset2 (nil) 127.0.0.1:6379> zcard testzset2 (integer) 128 127.0.0.1:6379> object encoding testzset2 "ziplist" 127.0.0.1:6379> zadd testzset2 3.14 pi (integer) 1 127.0.0.1:6379> zcard testzset2 (integer) 129 127.0.0.1:6379> object encoding testzset2 "skiplist"
以上两个条件中的阈值,能够经过配置文件中的如下选项进行修改:
zset-max-ziplist-entries 128 zset-max-ziplist-value 64
六:命令的执行
Redis中用于操做键的命令基本上能够分为两种类型:
一种命令能够对任何类型的键执行,好比DEL、EXPIRE、TYPE、OBJECT命令等;
另外一种命令只能对特定类型的键执行,好比:SET,GET,APPEND,STRLEN等命令只能对字符串键执行。
使用一种数据类型的特定命令操做另外一种数据类型的健会提示错误:”(error) WRONG TYPE Operation against a key holding the wrong kind ofvalue”。
所以,在执行一个类型特定的命令以前,Redis会先检查输人键的类型是否正确,而后再决定是否执行给定的命令。类型检查是经过redisObject结构的type属性来实现的。
Redis除了会根据值对象的类型来判断键是否可以执行指定命令以外,还会根据值对象的编码方式,选择正确的函数来执行命令。
好比,列表对象的编码有ziplist和linkedlist两种,若是对一个列表键执行LLEN命令,那么服务器须要根据键的值对象所使用的编码来选择正确的LLEN命令实现,若是编码为ziplist,将调用ziplistLen函数来返回压缩列表的长度;若是编码为linkedlist,将调用listLength函数来返回双端链表的长度。
七:引用计数
利用redisObject结构的引用计数,也就是refcount属性,能够实现内存回收,以及对象共享机制。
C语言不具有内存自动回收功能,因此Redis在本身的对象系统中构建了一个引用计数技术,实现内存回收机制。经过这一机制,程序能够经过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
对象的引用计数会随着对象的使用状态而不断变化:
建立一个新对象时,引用计数的值会被初始化为1;
当对象被一个新程序使用时,它的引用计数值会加1;
当对象再也不被一个程序使用时,它的引用计数值会减1;
当对象的引用计数值变为0时,对象所占用的内存会被释放。
除了用于实现内存回收机制以外,对象的引用计数属性还带有对象共享的做用。好比,假设键A建立了一个整数值100的字符串对象做为值对象,若是键B也要建立一个整数值100的字符串对象做为值对象,那么Redis会让键A和键B共享同一个字符串对象。这样作能够节约内存。
在 Redis中,让多个键共享同一个值对象,只须要将共享的值对象的引用计数加1便可。像前面介绍有序集合对象的skiplist编码时,dict和skiplist两种数据结构就共享同一个redisObject对象做为元素的成员。
默认状况下,Redis会在初始化服务器时,会建立10000个字符串对象,这些对象包含了从0到9999的全部整数值,当须要用到值为0到9 999的字符串对象时,服务器就会使用这些共享对象.而不是新建立对象。
好比:若是建立一个值为100的键a,并使用OBJECT REFCOUNT命令查看键a的值对象的引用计数,会发现值对象的引用计数为2,这就是由于键a的值,共享了Redis预先建立好的对象的缘故:
127.0.0.1:6379> set a 100 OK 127.0.0.1:6379> object refcount a (integer) 2
除了前面介绍过的type,encoding,ptr和refcount四个属性以外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被程序访问的时间。
OBJECT IDLETIME命令能够打印出给定键的空转时长,这一空转时长就是经过将当前时间减去键的值对象的lru时间计算得出的:
127.0.0.1:6379> set msg "hello" OK 127.0.0.1:6379> object idletime msg (integer) 6 127.0.0.1:6379> object idletime msg (integer) 23 127.0.0.1:6379> get msg "hello" 127.0.0.1:6379> object idletime msg (integer) 1
键的空转时长还有另一个做用,若是服务器打开了maxmemoxy选项,而且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的阈值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
对象相关的代码,能够参阅:
https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/src/object.c