通过对比lru时间与当前时间可以计算某个对象的空转时间object idletime命令可以显示该空转时间单位是秒。object idletime命令的一个特殊之处在于它不改变对象的lru值。lru值除了通过object idletime命令打印之外还与Redis的内存回收有关系如果Redis打开了maxmemory选项且内存回收算法选择的是volatile-lru或allkeys—lru那么当Redis内存占用超过maxmemory指定的值时Redis会优先选择空转时间最长的对象进行释放。4refcountrefcount与共享对象refcount记录的是该对象被引用的次数类型为整型占4个字节。refcount的作用主要在于对象的引用计数和内存回收。当创建新对象时refcount初始化为1当有新程序使用该对象时refcount加1当对象不再被一个新程序使用时refcount减1当refcount变为0时对象占用的内存会被释放。Redis中被多次使用的对象(refcount1)称为共享对象。Redis为了节省内存当有一些对象重复出现时新的程序不会创建新的对象而是仍然使用原来的对象。这个被重复使用的对象就是共享对象。目前共享对象仅支持整数值的字符串对象。共享对象的具体实现Redis的共享对象目前只支持整数值的字符串对象。之所以如此实际上是对内存和CPU时间的平衡共享对象虽然会降低内存消耗但是判断两个对象是否相等却需要消耗额外的时间。对于整数值判断操作复杂度为O(1)对于普通字符串判断复杂度为O(n)而对于哈希、列表、集合和有序集合判断的复杂度为O(n^2)。虽然共享对象只能是整数值的字符串对象但是5种类型都可能使用共享对象如哈希、列表等的元素可以使用。就目前的实现来说Redis服务器在初始化时会创建10000个字符串对象值分别是0~9999的整数值当Redis需要使用值为0~9999的字符串对象时可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS4.0中是OBJ_SHARED_INTEGERS的值进行改变。共享对象的引用次数可以通过object refcount命令查看如下图所示。命令执行的结果页佐证了只有0~9999之间的整数会作为共享对象。5ptrptr指针指向具体的数据如前面的例子中set hello worldptr指向包含字符串world的SDS。ptr指针占据的字节数与系统有关例如64位系统中占8个字节。6总结综上所述redisObject的结构与对象类型、编码、内存回收、共享对象都有关系在64位系统中一个redisObject对象的大小为16字节4bit4bit24bit4Byte8Byte16Byte。4、SDSRedis没有直接使用C字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示而是使用了SDS。SDS是简单动态字符串(Simple Dynamic String)的缩写。1SDS结构sds的结构如下12345structsdshdr {intlen;intfree;charbuf[];};其中buf表示字节数组用来存储字符串len表示buf已使用的长度free表示buf未使用的长度。下面是两个例子。图片来源《Redis设计与实现》通过SDS的结构可以看出buf数组的长度freelen1其中1表示字符串结尾的空字符所以一个SDS结构占据的空间为free所占长度len所占长度 buf数组的长度44freelen1freelen9。2SDS与C字符串的比较SDS在C字符串的基础上加入了free和len字段带来了很多好处获取字符串长度SDS是O(1)C字符串是O(n)缓冲区溢出使用C字符串的API时如果字符串长度增加如strcat操作而忘记重新分配内存很容易造成缓冲区的溢出而SDS由于记录了长度相应的API在可能造成缓冲区溢出时会自动重新分配内存杜绝了缓冲区溢出。修改字符串时内存的重分配对于C字符串如果要修改字符串必须要重新分配内存先释放再申请因为如果没有重新分配字符串长度增大时会造成内存缓冲区溢出字符串长度减小时会造成内存泄露。而对于SDS由于可以记录len和free因此解除了字符串长度和空间数组长度之间的关联可以在此基础上进行优化空间预分配策略即分配内存时比实际需要的多使得字符串长度增大时重新分配内存的概率大大减小惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。存取二进制数据SDS可以C字符串不可以。因为C字符串以空字符作为字符串结束的标识而对于一些二进制文件如图片等内容可能包括空字符串因此C字符串无法正确存取而SDS以字符串长度len来作为字符串结束标识因此没有这个问题。此外由于SDS中的buf仍然使用了C字符串即以’\0’结尾因此SDS可以使用C字符串库中的部分函数但是需要注意的是只有当SDS用来存储文本数据时才可以这样使用在存储二进制数据时则不行’\0’不一定是结尾。3SDS与C字符串的应用Redis在存储对象时一律使用SDS代替C字符串。例如set hello world命令hello和world都是以SDS的形式存储的。而sadd myset member1 member2 member3命令不论是键”myset”还是集合中的元素”member1”、 ”member2”和”member3”都是以SDS的形式存储。除了存储对象SDS还用于存储各种缓冲区。只有在字符串不会改变的情况下如打印日志时才会使用C字符串。四、Redis的对象类型与内部编码前面已经说过Redis支持5种对象类型而每种结构都有至少两种编码这样做的好处在于一方面接口与实现分离当需要增加或改变内部编码时用户使用不受影响另一方面可以根据不同的应用场景切换内部编码提高效率。Redis各种对象类型支持的内部编码如下图所示(图中版本是Redis3.0Redis后面版本中又增加了内部编码略过不提本章所介绍的内部编码都是基于3.0的)图片来源《Redis设计与实现》关于Redis内部编码的转换都符合以下规律编码转换在Redis写入数据时完成且转换过程不可逆只能从小内存编码向大内存编码转换。1、字符串1概况字符串是最基础的类型因为所有的键都是字符串类型且字符串之外的其他几种复杂类型的元素也是字符串。字符串长度不能超过512MB。2内部编码字符串类型的内部编码有3种它们的应用场景如下int8个字节的长整型。字符串值是整型时这个值使用long整型表示。embstr39字节的字符串。embstr与raw都使用redisObject和sds保存数据区别在于embstr的使用只分配一次内存空间因此redisObject和sds是连续的而raw需要分配两次内存空间分别为redisObject和sds分配空间。因此与raw相比embstr的好处在于创建时少分配一次空间删除时少释放一次空间以及对象的所有数据连在一起寻找方便。而embstr的坏处也很明显如果字符串的长度增加需要重新分配内存时整个redisObject和sds都需要重新分配空间因此redis中的embstr实现为只读。raw大于39个字节的字符串示例如下图所示embstr和raw进行区分的长度是39是因为redisObject的长度是16字节sds的长度是9字符串长度因此当字符串长度是39时embstr的长度正好是1693964jemalloc正好可以分配64字节的内存单元。3编码转换当int数据不再是整数或大小超过了long的范围时自动转化为raw。而对于embstr由于其实现是只读的因此在对embstr对象进行修改时都会先转化为raw再进行修改因此只要是修改embstr对象修改后的对象一定是raw的无论是否达到了39个字节。示例如下图所示2、列表1概况列表list用来存储多个有序的字符串每个字符串称为元素一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入和弹出并可以获得指定位置或范围的元素可以充当数组、队列、栈等。2内部编码列表的内部编码可以是压缩列表ziplist或双端链表linkedlist。双端链表由一个list结构和多个listNode结构组成典型结构如下图所示图片来源《Redis设计与实现》通过图中可以看出双端链表同时保存了表头指针和表尾指针并且每个节点都有指向前和指向后的指针链表中保存了列表的长度dup、free和match为节点值设置类型特定函数所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。压缩列表压缩列表是Redis为了节约内存而开发的是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构具体结构相对比较复杂略。与双端链表相比压缩列表可以节省内存空间但是进行修改或增删操作时复杂度较高因此当节点数量较少时可以使用压缩列表但是节点数量多时还是使用双端链表划算。压缩列表不仅用于实现列表也用于实现哈希、有序列表使用非常广泛。3编码转换只有同时满足下面两个条件时才会使用压缩列表列表中元素数量小于512个列表中所有字符串对象都不足64字节。如果有一个条件不满足则使用双端列表且编码只可能由压缩列表转化为双端链表反方向则不可能。下图展示了列表编码转换的特点其中单个字符串不能超过64字节是为了便于统一分配每个节点的长度这里的64字节是指字符串的长度不包括SDS结构因为压缩列表使用连续、定长内存块存储字符串不需要SDS结构指明长度。后面提到压缩列表也会强调长度不超过64字节原理与这里类似。3、哈希1概况哈希作为一种数据结构不仅是redis对外提供的5种对象类型的一种与字符串、列表、集合、有序结合并列也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便在本文后面当使用“内层的哈希”时代表的是redis对外提供的5种对象类型的一种使用“外层的哈希”代指Redis作为Key-Value数据库所使用的数据结构。2内部编码内层的哈希使用的内部编码可以是压缩列表ziplist和哈希表hashtable两种Redis的外层的哈希则只使用了hashtable。压缩列表前面已介绍。与哈希表相比压缩列表用于元素个数少、元素长度小的场景其优势在于集中存储节省空间同时虽然对于元素的操作复杂度也由O(1)变为了O(n)但由于哈希中元素数量较少因此操作的时间并没有明显劣势。hashtable一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组称为bucket和多个dictEntry结构组成。正常情况下即hashtable没有进行rehash时各部分关系如下图所示图片改编自《Redis设计与实现》下面从底层向上依次介绍各个部分dictEntrydictEntry结构用于保存键值对结构定义如下123456789typedefstructdictEntry{void*key;union{void*val;uint64_tu64;int64_ts64;}v;structdictEntry *next;}dictEntry;其中各个属性的功能如下key键值对中的键val键值对中的值使用union(即共用体)实现存储的内容既可能是一个指向值的指针也可能是64位整型或无符号64位整型next指向下一个dictEntry用于解决哈希冲突问题在64位系统中一个dictEntry对象占24字节key/val/next各占8字节。bucketbucket是一个数组数组的每个元素都是指向dictEntry结构的指针。redis中bucket数组的大小计算规则如下大于dictEntry的、最小的2^n例如如果有1000个dictEntry那么bucket大小为1024如果有1500个dictEntry则bucket大小为2048。dicthtdictht结构如下123456typedefstructdictht{dictEntry **table;unsignedlongsize;unsignedlongsizemask;unsignedlongused;}dictht;其中各个属性的功能说明如下table属性是一个指针指向bucketsize属性记录了哈希表的大小即bucket的大小used记录了已使用的dictEntry的数量sizemask属性的值总是为size-1这个属性和哈希值一起决定一个键在table中存储的位置。dict一般来说通过使用dictht和dictEntry结构便可以实现普通哈希表的功能但是Redis的实现中在dictht结构的上层还有一个dict结构。下面说明dict结构的定义及作用。dict结构如下123456typedefstructdict{dictType *type;void*privdata;dictht ht[2];inttrehashidx;} dict;其中type属性和privdata属性是为了适应不同类型的键值对用于创建多态字典。ht属性和trehashidx属性则用于rehash即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组每项都指向一个dictht结构这也是Redis的哈希会有1个dict、2个dictht结构的原因。通常情况下所有的数据都是存在放dict的ht[0]中ht[1]只在rehash的时候使用。dict进行rehash操作的时候将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0]并清空ht[1]。因此Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构一方面是为了适应不同类型的键值对另一方面是为了rehash。3编码转换如前所述Redis中内层的哈希既可能使用哈希表也可能使用压缩列表。只有同时满足下面两个条件时才会使用压缩列表哈希中元素数量小于512个哈希中所有键值对的键和值字符串长度都小于64字节。如果有一个条件不满足则使用哈希表且编码只可能由压缩列表转化为哈希表反方向则不可能。下图展示了Redis内层的哈希编码转换的特点4、集合1概况集合set与列表类似都是用来保存多个字符串但集合与列表有两点不同集合中的元素是无序的因此不能通过索引来操作元素集合中的元素不能有重复。一个集合中最多可以存储2^32-1个元素除了支持常规的增删改查Redis还支持多个集合取交集、并集、差集。2内部编码集合的内部编码可以是整数集合intset或哈希表hashtable。哈希表前面已经讲过这里略过不提需要注意的是集合在使用哈希表时值全部被置为null。整数集合的结构定义如下12345typedefstructintset{uint32_t encoding;uint32_t length;int8_t contents[];} intset;其中encoding代表contents中存储内容的类型虽然contents存储集合中的元素是int8_t类型但实际上其存储的值是int16_t、int32_t或int64_t具体的类型便是由encoding决定的length表示元素个数。整数集合适用于集合所有元素都是整数且集合元素数量较小的时候与哈希表相比整数集合的优势在于集中存储节省空间同时虽然对于元素的操作复杂度也由O(1)变为了O(n)但由于集合数量较少因此操作的时间并没有明显劣势。3编码转换只有同时满足下面两个条件时集合才会使用整数集合集合中元素数量小于512个集合中所有元素都是整数值。如果有一个条件不满足则使用哈希表且编码只可能由整数集合转化为哈希表反方向则不可能。下图展示了集合编码转换的特点