来源:blog.csdn.net/zwx900102/article/details/113096979前言在Redis中,有一种数据类型在存储的时候使用两种数据结构分别存储,那么Redis为什么要这样做呢?做吗?这样做会不会导致同样的数据占用两倍的空间?集合对象的五种基本类型Redis中的集合对象是一个无序的集合,包含字符串类型的元素,集合中唯一的元素不能重复。集合对象有两种底层数据结构:intset和hashtable。内部通过编码区分:intset编码intset(整数集)可以存储int16_t、int32_t、int64_t类型的整数值,并保证集合中没有重复的元素。intset数据结构定义如下(在源码inset.h中):集合中的特定元素}intset;下图是一个intset集合对象存储的简化图:encodingintset内部的encoding记录了当前整数集合的数据存储类型。主要有三种类型:INTSET_ENC_INT16此时contents[]中的每个元素都是一个int16_t类型的整数值,范围是:-32768~32767(-2的15次方~2的15次方-1)。INTSET_ENC_INT32此时contents[]中的每个元素都是一个int32_t类型的整数值,取值范围为:-2147483648~2147483647(-2的31次方~2的31次方-1)。INTSET_ENC_INT64此时contents[]中的每个元素都是一个int64_t类型的整数值,取值范围为:-9223372036854775808~9223372036854775807(-2的63次方~2的63次方-1)。contents[]contents[]虽然结构体的定义写成int8_t类型,但是实际存储类型是由上面的编码决定的。整数集的升级如果整数集中的元素一开始都是16位的,则以int16_t类型存储。这时需要存储一个32位的整数,所以需要对原有的整数集进行升级。升级后,32位整数存储在整数集合中。这就涉及到整数集合的类型升级。升级过程主要有4个步骤:根据新增元素的类型扩展底层数组空间的大小,升级后根据已有元素的位数分配新的空间。对已有元素进行类型转换,将转换后的元素从后往前一个一个放回数组中。将新元素放在数组的头部或尾部(因为触发升级的条件是当前数组的整数类型不能存储新元素,所以新元素要么大于现有元素,要么小于现有元素)。将encoding属性更改为最新的编码,同步修改length属性。PS:和字符串对象的编码一样,整数集合的类型一旦升级,将保持编码状态,无法降级。升级示例1.假设我们有一个集合,其编码为int16_t,里面存储了3个元素:2.这时候我们需要插入一个整数50000,但是发现存储不出来,而50000是一个整数类型是int32_t,所以我们需要申请一个新的空间,申请空间的大小是4*32-48=80。3、现在新数组有4个元素要放,原数组排在第3位,所以升级后的3需要移动到64-95。4.继续将升级后的2移动到32-63位。5.继续将升级后的1移动到位0-31。6.然后将50000放入第96-127位。7、最后会修改encoding和length属性,修改完成后升级完成。hashtableencodinghashtable结构在我们讲hash对象的时候已经详细分析过了。如果您想了解更多信息,可以单击此处。Intset和hashtable编码转换当一个集合满足以下两个条件时,Redis会选择使用intset编码:集合对象中存储的所有元素都是整型值。集合对象保存的元素个数小于等于512(这个阈值可以通过配置文件set-max-intset-entries控制)。一旦集合中的元素不满足以上两个条件,就会选择使用hashtable编码。集合对象常用命令saddkeymember1member2:向集合key中添加一个或多个元素成员,并返回添加成功的次数,如果该元素已经存在则忽略。sismemberkeymember:判断集合key中是否存在元素成员。sremkeymember1member2:移除集合key中的元素,不存在的元素将被忽略。smovesourcedestmember:将集合source中的元素成员移动到dest,如果该成员不存在,什么都不做。smemberskey:返回集合key中的所有元素。知道了操作集合对象的常用命令,我们就可以验证上面提到的哈希对象的类型和编码了。在测试之前,为了防止其他键值的干扰,我们先执行flushall命令清空Redis数据库。依次执行以下命令:saddnum123//设置一组3个整数,并使用intset编码类型num//检查类型对象编码num//检查编码saddname123test//设置3integersand1strings的集合会使用hashtable编码typename//Viewtypeobjectencodingname//检查编码得到如下效果:可以看到当set元素中只有整数时,set使用intset编码,当集合元素包含非整数时,使用哈希表编码。有序集合对象的五种基本类型有序集合与Redis中的集合的区别在于,有序集合中的每个元素都会关联一个double类型的分数,然后按照分数的升序排列。也就是说,有序集的顺序是由我们自己设置值时的分数决定的。有序集合对象有两种底层数据结构:skiplist和ziplist。内部也是通过编码来区分的:skiplistencodingskiplist就是跳跃列表,有时也简称为跳表。使用skiplist编码的有序集合对象使用zset结构作为底层实现,zset同时包含字典和skiplist。跳转表跳转表是一种有序的数据结构,其主要特点是通过在每个节点中维护多个指向其他节点的指针来达到快速访问节点的目的。大多数情况下,跳表的效率可以和平衡树相当,但是跳表的实现远比平衡树简单,所以Redis选择使用跳表来实现有序集合。下图是一个普通的有序链表。如果我们要找到第35个元素,只能从头到尾遍历(链表中的元素不支持随机访问,所以不能使用二分查找,可以通过数组中的下标进行随机访问.,所以二分查找一般适用于有序数组),时间复杂度为O(n)。那么如果我们可以直接跳转到链表的中间,就可以节省很多资源。这就是跳表的原理。每个级别都有一个指向同一级别下一个元素的指针。比如我们遍历找到上图中的35号元素,有3种选择:第一种是执行level1的指针,需要遍历7次(1->8->9->12->15->20->35)找到元素35。第二种是实现level2级指针,只需要遍历5次(1->9->12->15->35)找到元素35。第三种类型是实施3级要素。这时只需要遍历3次(1->12->35)就可以找到第35个元素,大大提高了效率。Skiplist存储结构skiplist中的每个节点都是一个zskiplistNode节点(在源码server.h中):{//layerstructzskiplistNode*forward;//forwardpointerunsignedlongspan;//从当前节点到下一个节点的span(跨越的节点数)}level[];}zskiplistNode;level(layer)层级是skiptable中的层是一个数组,也就是说一个节点的一个元素可以有多个层,也就是多个指向其他节点的指针。程序可以通过不同层次的指针选择最快的路径来提高访问速度。level是每次创建新节点时根据幂律随机生成的1到32之间的数字。forward(前向指针)每一层都会有一个指向链表尾部元素的指针,遍历元素时需要用到前向指针。跨度(span)跨度记录了两个节点之间的距离。需要注意的是,如果指向NULL,则跨度为0。向后(向后指针)和向前指针的区别在于向后指针只有一个,所以一次只能回到上一个节点(上图中没有绘制反向指针)。ele(元素)跳表中的元素是一个sds对象(早期版本使用的是redisObject对象),元素必须是唯一的,不能重复。score(score)节点的分数是一个double类型的浮点数。跳表会根据分数升序排列节点,不同节点的分数可以重复。上面只是跳表中的一个节点,多个zskiplistNode节点组成一个zskiplist对象:/跳表的节点数intlevel;//所有节点中最大的层数}zskiplist;你可能认为有序集合就是通过这个zskiplist来实现的,但实际上Redis并没有直接使用zskiplist来实现,而是用一个zset对象再次包裹起来。typedefstructzset{dict*dict;//字典对象zskiplist*zsl;//跳过列表对象}zset;所以最后,如果一个有序集合使用skiplist编码,它的数据结构如下图所示:上图中上半部分的字典中的key对应的是有序集合中的元素(成员),和值对应的是分数(score)。上图下半部分,跳转表中的整数1、8、9、12也对应着元素(成员),最后一行的双数是分数。也就是说,字典和跳表中的数据都指向我们存储的元素(两个数据结构最终指向同一个地址,所以数据不会冗余存储),Redis为什么要这样做呢?为什么选择同时使用字典和跳转列表有序集合可以直接使用跳转列表或者单独使用字典来实现,但是大家想一想,如果单独使用跳转列表,那么虽然可以使用大跨度的指针来遍历elements来找到我们需要的数据,但是它的复杂度还是达到了O(logN),而在字典中获取一个元素的复杂度是O(1),如果单独使用字典,虽然获取元素很快,但是字典是无序的,所以如果要按范围查找,就需要对它们进行排序,这又是一个耗时的操作,所以Redis结合了两种数据结构来最大化性能,这也是Redis设计的精妙之处。ziplist编码的压缩列表用于列表对象和哈希对象。如果您想了解更多信息,可以单击此处。https://blog.csdn.net/zwx9001...ziplist和skiplist编码转换当有序集合对象同时满足以下两个条件时,将使用ziplist编码存储:有序集合中存储的元素个数collectionobject小于128(可以通过配置zset-max-ziplist-entries修改)。已排序集合对象中存储的所有元素的总长度小于64字节(可以通过配置zset-max-ziplist-value修改)。有序集合对象常用命令zaddkeyscore1member1score2member2:向有序集合key中添加一个或多个元素(member)及其分数。zscorekeymember:返回成员member在有序集合key中的分数。zincrbykeynummember:给有序集合key中的成员加上num,num可以为负数。zcountkeyminmax:返回排序集合key中score值在[min,max]范围内的成员个数。zrangekeystartstop:返回sortedsetkey中score从小到大排列后[start,stop]区间内的所有成员。zrevrangekeystartstop:返回sortedsetkey中score从大到小排列后[start,stop]范围内的所有成员。zrangebyscorekeyminmax:返回sortedset中[min,max]区间内的所有元素,按score从小到大排序。注意默认是闭区间,但是可以在max和min的值前加上(或者[来控制开闭区间。zrevrangebyscorekeymaxmin:返回按照scorefrom排序的set从大到小然后在[min,max]区间内的所有元素。注意默认是闭区间,但是可以在max和min值前面加上(或者[来控制开区间和闭区间.zrankkeymember:返回成员中元素在有序集合中的排名(从小到大),返回结果从0开始计数。zrevrankkeymember:返回有序集合中member中元素的排名(从大到小),返回结果从0开始计算。zlexcountkeyminmax:返回有序集合中min和max之间的成员个数.注意该命令中的min和max前面必须加上(或[来控制开闭区间,特殊值-和+分别代表负无穷大和正无穷大。知道操作有序集合对象的常用命令,我们可以验证一下前面提到的hash对象的类型和编码,在测试之前为了防止其他key值的干扰,我们先执行flushall命令清空Redis数据库,在执行命令之前,我们先在配置文件中设置参数zset-max-ziplist将-entries改为2,然后重启Redis服务,重启完成后依次执行以下命令:zaddname1zs2lisi//设置2个元素将useziplisttypename//查看类型对象编码名称//查看编码zaddaddress1beijing2shanghai3guangzhou4shenzhen//设置4个元素将使用skiplist编码类型地址//查看类型对象编码地址//查看编码得到效果如下:总结本文主要分析了集合对象和有序集合对象的底层存储结构intset和skiplist的实现原理,重点分析了有序集合是如何排序的,为什么要用两种数据结构(字典和skiplist)同时用于存储数据。近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.厉害了!Java协程来了...3.SpringBoot2.x教程,太全面了!4.别再写爆满屏的类了,试试装饰者模式吧,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!
