90%的人知道Redis最基本的5种数据结构,只有不到10%的人知道8种。基础数据结构(5个基础+bitmap+GeoHash+HyperLogLog),不到5%的人知道9个基础数据结构(最新5.0版本的数据结构Streams),不到1%的人掌握全部9个基础数据结构数据结构与8种内部编码,掌握本文知识点,让你成为面试官眼中Redis最美少年!注:本文基于Redis-3.2.11版本源码分析。5种常见的数据结构就没什么好说的了。稍微了解Redis的人都知道5种最基本的数据结构:String、List、Hash、Set、SortedSet。不过需要注意的是,这里还有几个高频面试题。Set和Hash的关系答案是Set是一个特殊的Hash,它的值为空。Set类型操作的源代码在t_set.c中。以添加一个元素为例(intsetTypeAdd(robj*subject,sdsvalue)),如果编码类型是OBJ_ENCODING_HT,那么新源码的源码如下,其实就是对dict进行操作或Hash数据结构,dictSetVal值为NULL:dictEntry*de=dictAddRaw(ht,value,NULL);if(de){dictSetKey(ht,de,sdsdup(value));dictSetVal(ht,de,NULL);return1;}同样,我们在t_hash.c中看到Hash类型的新元素时,确定编??码类型为OBJ_ENCODING_HT时,也是调用dict的方法:dictAdd(o->ptr,f,v),dictAdd最后调用了dictSetVal()方法,但是v表示值不为NULL:/*Addanelementtothetargethashtable*/intdictAdd(dict*d,void*key,void*val){dictEntry*entry=dictAddRaw(d,key,NULL);if(!entry)returnDICT_ERR;dictSetVal(d,entry,val);returnDICT_OK;}所以Redis中Set和Hash的关系就很清楚了。当编码为OBJ_ENCODING_HT时,都是dict数据类型,但Set是一个特殊的dict,值为NULL。谈谈你对SortedSet的理解。SortedSet的数据结构是一个跳表,即SkipList。用于排序的因子得分。因此,局限性非常大。举个栗子:热榜需要按下载量&最近更新时间排序,即类似于数据库中的ORDERBYdownload_count,update_timeDESC。怎么样用Redis的SortedSet来实现这样的需求呢?其实很简单。思路是将参与排序的多个维度的列按照一定的方式转化为一个特殊的列,即result=function(x,y,z),即x,y,z为三个排序因子,如下载量、时间等,通过自定义函数function()计算结果,将结果作为SortedSet中score的值,实现任意维度的排序。Redis内部编码我们常说String、List、Hash、Set、SortedSet只是外部编码。实际上,每种数据结构都有自己底层的内部编码实现,并且有多种实现方式,这样才能在合适的场景下使用Redis。选择更合适的内部编码。如下图(图片更正:intset编码,不是inset编码),可以看到每个数据结构都有不止两种内部编码实现。例如,String数据结构包含三种内部编码:raw、int和embstr。同时,一些内部代码可以作为各种外部数据结构的内部实现。比如ziplist是hash,list,zset共享的内码,set的内码可能是hashtable或者intset:Redis内码Redis在这个设计上有两个好处:内码可以在不影响外码的情况下偷偷改进数据结构和命令,一旦开发出更好的内部代码,就不需要改变外部数据结构和命令。多种内部编码实现可以在不同的场景下发挥各自的优势。例如,ziplist可以节省内存,但是当列表中的元素很多时,它的性能会下降。这时候Redis会根据配置选项将list类型的内部实现转换为linkedlist。从上图可以看出String的三种内部编码。String的三种内部编码分别是:int、embstr、raw。int类型很容易理解。当key的值为整数时,Redis会把它编码为int类型(还有一个条件:把这个值当成字符串,长度不能超过20)。如下。这种编码类型是为了节省内存。默认情况下,Redis会缓存10000个整数值(#defineOBJ_SHARED_INTEGERS10000),也就是说如果有10个不同的KEY,它们的值都在10000以内。事实上,它们都共享同一个对象:127.0.0。0.1:6379>setnumber"7890"OK127.0.0.1:6379>objectencodingnumber"int"接下来是ebmstr和raw内部编码的长度限制,请看下面源码:#defineOBJ_ENCODING_EMBSTR_SIZE_LIMIT44robj*createStringObject(constchar*ptr,size_tlen){if(len<=OBJ_ENCODING_EMBSTR_SIZE_LIMIT)returncreateEmbeddedStringObject(ptr,len);elsereturncreateRawStringObject(ptr,len);}也就是说embstr和raw编码的长度限制为44,我们可以做如下验证。长度超过44后,就是没有做任何优化的原始编码类型。该长度会占用大量内存:127.0.0.1:6379>setname"a1234567890123456789012345678901234567890123"OK127.0.0.1:6379>objectencodingname"embstr"127.0。0.1:6379>setname"a12345678901234567890123456789012345678901234"OK127.0.0.1:6379>objectencodingname"raw"那么为什么会有embstr编码呢?它比生的有什么优势?embstr编码会将创建字符串对象所需的空间分配数量从两种原始编码减少到一种。因为embstr编码的字符串对象的所有数据都存储在连续的内存中,所以这个编码的字符串对象可以比原始编码的字符串对象更好地利用缓存。而释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象则需要调用两次内存释放函数。如下图,左边是embstr代码,右边是原始代码:embstrV.S.rawziplist从上图可以看出,外部结构有List、Hash、SortedSet三种。特殊情况下,内部代码是ziplist,那么这个ziplist有什么魔力呢?以Hash为例,我们先看看其内部编码为ziplist的条件:当hash类型元素个数小于hash-max-ziplist-entries配置(默认为512)时;所有值都小于hash-max-ziplist-value配置(默认64字节);如果是sortedset,还需要满足两个条件:元素个数小于zset-max-ziplist-entries配置,默认128个;所有值都小于zset-max-ziplist-value配置,默认64。其实ziplist充分体现了Redis对存储效率的追求。一个普通的双向链表,链表中的每一项都占据一个独立的内存块,各项之间通过地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,地址指针也会额外占用内存。但是ziplist将表中的每一项存储在连续的地址空间中,一个ziplist整体占用内存较大。它是列表(list),但不是链表(linkedlist)。ziplist的源代码在ziplist.c文件中,里面有一段描述——ziplist的大体布局如下:
