当前位置: 首页 > 科技观察

今天刚了解到Redis有9种基本数据类型,据说只有5%的人知道

时间:2023-03-13 11:55:44 科技观察

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的大体布局如下:...zlbytes:表示这个ziplist占用了多少空间,或者说占用了多少字节,包括zlbytes本身占用的4个字节;zltail:表示到ziplist中最后一个元素的偏移量,如果超过这个值,pop操作的时间复杂度为O(1),即不需要遍历整个ziplist;zllen:表示ziplist中有多少条目,即保存了多少元素。由于该字段占16个字节,最大值为2^16-1,也就是说如果条目数超过2^16-1,则需要遍历整个ziplist才能知道条目数;entry:真正保存的数据有自己的编码;zlend:专门用来表示ziplist末尾的特殊字符,占用8个字节,取值固定为255,即8个字节的每一位都为1。下面是一个真正的ziplist代码,其中包含2和5的两个元素:[0f000000][0c000000][0200][00f3][02f6][ff]|||||zlbyteszltailentries"2""5"endlinkedlist,这是List的编码数据结构。很简单,就是我们非常熟悉的双向链表,对应Java中的LinkedList。前面也提到了skiplist,这是经典的skiplist数据结构。Hashtable也很容易,对应Java中的HashMap。intsetSet有一个特殊的内部编码。当满足以下条件时,Set的内部编码为intset而不是hashtable:Set集合必须是64位有符号十进制整数;元素个数不能超过set-max-intset-entries配置,默认512;验证如下:127.0.0.1:6379>saddscores135(integer)0127.0.0.1:6379>saddscores128(integer)1127.0.0.1:6379>objectencodingscores"intset"那么到底什么是intset编码呢?看它的源码定义如下,很明显,是一个整数数组,而且是一个有序的整数数组。它在内存分配上有点类似于ziplist,是一块连续的内存空间,并且对大整数和小整数采用不同的编码,尽可能优化内存的使用。对于这样的数据结构,如果执行SISMEMBER命令,即检查一个元素是否在集合中,实际上是使用二分查找的方式:typedefstructintset{uint32_tencoding;uint32_tlength;int8_tcontents[];}intset;//intset编码搜索方法源码(手动简化),标准二分搜索方法:staticuint8_tintsetSearch(intset*is,int64_tvalue,uint32_t*pos){intmin=0,max=intrev32ifbe(is->length)-1,mid=-1;int64_tcur=-1;while(max>=min){mid=((unsignedint)min+(unsignedint)max)>>1;cur=_intsetGet(is,mid);if(value>cur){min=mid+1;}elseif(valueGEOADDcity114.03104022.324386"shenzhen"112.57215422.267832"guangzhou"(integer)2redis>GEODISTcityshenzhenguangzhou"150265.8106"但是需要注意的是,Geo本身并不是一个数据结构,并且它本质上依赖于SortedSet(ZSET),并使用GeoHash技术来填充。在Redis中,经纬度用52位整数编码后放入zset,score就是GeoHash的52位整数值。在使用Redis进行Geo查询时,对应的内部操作其实就是zset(skiplist)的操作。通过对zset的score进行排序,可以得到该坐标附近的其他元素,通过将score还原为坐标值,可以得到该元素的原始坐标。简而言之,Redis中处理这些地理位置坐标点的思路是:二维平面坐标点-->一维整数编码值-->zset(score为编码值)-->zrangebyrank(获取元素withsimilarscore),zrangebyscore-->通过分数(整数编码值)解码坐标点-->附近点的地理坐标。GEOHASH原理使用wiki上的例子。纬度为42.6,经度为-5.6。如何将其转换为base32?首先,让我们解释一下纬度。纬度范围是-90到90,把这个范围分成两段,分别是[-90,0],[0,90],然后看给定的纬度在哪个范围,如果在前一个范围,将当前位设置为0,再将值设置为1。然后继续设置确定的范围1分为2,通过判断值是在前段还是后段来确定该位的值.这样,范围就慢慢缩小了,一般最多13倍(经纬度的二进制数加起来是25位,经度是13位,纬度是12位)。此时的中间值将是最接近给定值的。如下图所示:Geohash第1行,纬度42.6在[0,90]之间,所以bit=1;第2行,纬度42.6在[0,45]之间,所以bit=0;第3行,纬度42.6在[22.5,45]之间,所以bit=1,以此类推。这样,取出图中的位:101111001001,用同样的方法计算经度(范围-180到180)为:0111110000000结果如下:#Longitude0111110000000#Latitude101111001001得到经纬度的二进制数后,下面需要将两者结合:从经纬度的循环中,每次取一个二进制位(不足位取0),合并为新的二进制数:0110111111110000010000010.每5位是一个十进制数,结合base32对应表,base32值映射为:ezs42。这样就完成了编码过程。Streams是Redis5.0引入的一种新的数据结构。一句话,Streams就是Redis实现的内存版Kafka。而且Streams还有ConsumerGroups的概念。从Redis源码中stream的定义可知,streams的底层数据结构是radixtree:*/rax*cgroups;/*Consumergroupsdictionary:name->streamCG*/}stream;那么这棵基数树长什么样子呢?Redis源码的rax.h文件中有这样的描述,所以看起来更直观:(f)""*/*(io)"f"*/\*"firsts"("rst")(o)"fo"*/\*"first"[][tb]"foo"*/\*"foot"("er")("ar")"foob"*/\*"footer"[][]"foobar"RadixTree(基数树)其实和传统的二叉树差不多。只是在查找方法中,以一个unsignedint类型的数为例,将这个数的每一位作为树节点的推理。可以说,比如一个数10001010101010110101010,那么根据Radix树,插入是在根节点。如果遇到0,则指向左节点。如果遇到1,则指向正确的节点。在插入过程中构建树节点,在删除过程中删除树节点。下面是保存7个单词的RadixTree: