当前位置: 首页 > 后端技术 > Java

通俗易懂的Redis数据结构基础教程

时间:2023-04-01 15:52:37 Java

Redis有5种基本数据结构,string、list、hash、set和zset。它们是日常开发中使用频率非常高的最广泛的数据结构。如果你把这五种数据结构都吃透了,你就掌握了一半的Redis应用知识。字符串让我们先从字符串开始。String表示可变字节数组。我们初始化字符串的内容,获取字符串的长度,获取字符串的子串,覆盖字符串的子串内容,追加子串。Redis字符串是可以修改的动态字符串。内部结构类似于Java的ArrayList。它使用预先分配的冗余空间来减少内存的频繁分配。如图所示,内部是当前字符串实际分配的空间容量,一般要高于实际字符串长度len。当字符串长度小于1M时,扩容会将现有空间扩大一倍。如果超过1M,扩容一次只会扩容1M空间。需要注意的是字符串最大长度为512M。初始化字符串需要提供“变量名”和“变量内容”>setireaderbeijing.zhangyue.keji.gufen.youxian.gongsiOK复制代码获取字符串内容提供“变量名”>获取ireader"beijing.zhangyue.keji.gufen.youxian.gongsi"复制代码获取字符串长度Provide"variablename">strlenireader(integer)42复制代码获取子字符串Provide"variablename"andstart和结束位置[start,end]>getrangeireader2834"youxian"复制代码覆盖子串提供“变量名”和起始位置和目标子串>setrangeireader28wooxian(integer)42#returnlengthgetireader"beijing.zhangyue.keji.gufen.wooxian.gongsi"copycodeAppendsubstring>appendireader.hao(integer)46#returnlengthgetireader"beijing.zhangyue.keji.gufen.wooxian.gongsi.hao"Copycode可惜字符串没有提供字符串插入方法和子字符串删除方法。计数器如果字符串的内容是整数,也可以将字符串作为计数器。>setireader42OKgetireader"42"incrbyireader100(integer)142getireader"142"decrbyireader100(integer)42getireader"42"incrireader#equivalenttoincrbyireader1(integer)43decrireader#equivalenttodecrbyireader1(integer)42拷贝码计数器有一个范围,不能超过Long.Max,也不能低于Long.MIN>setireader9223372036854775807OKincrireader(error)ERR递增或递减会溢出setireader-9223372036854775808OKdecrireader(error)递增或ERR递减会溢出复制代码过期删除字符串可以使用del命令主动删除,过期时间可以使用expire命令设置,到点自动删除,是被动的删除。可以使用ttl命令获取字符串的生命周期。>expireireader60(integer)1#1表示设置成功,0表示变量ireader不存在ttlireader(integer)50#还有50秒的生命,返回-2表示变量不存在,-1表示没有设置过期时间delireader(integer)1#删除成功返回1getireader(nil)#变量ireader没有了双向链表。因为是链表,随机定位性能较弱,端到端的插入删除性能较好。如果链表的链表长度很长,我们在使用的时候一定要注意链表相关操作的时间复杂度。负下标链表元素的位置用自然数0,1,2,....n-1表示,也可以用负数-1,-2,...-n,-1表示“最后一个”,-2表示“倒数第二个”,那么-n表示第一个元素,对应的下标为0。Queue/Stack链表可以对链表的头部和尾部添加和移除元素,结合使用rpush/rpop/lpush/lpop这4条指令可以将链表作为队列或栈来使用,可以是从左到右#右进左出rpushireadergo(integer)1rpushireaderjavapython(integer)3lpopireader"go"lpopireader"java"lpopireader"python"leftinrightoutlpushireadergojavapython(integer)3rpopireader"go"...右进右出rpushireadergojavapython(integer)3rpopireader"python"...左进左出lpushireadergojavapython(integer)3lpopireader"python"...复制代码在日常应用中,列表经常被当作异步Queue来使用。length使用llen命令获取链表长度>rpushireadergojavapython(integer)3llenireader(integer)3复制随机读取的代码可以使用lindex命令访问指定位置的元素,使用lrange命令获取链表的子元素列表,并提供start和end下标参数>rpushireadergojavapython(integer)3lindexireader1"java"lrangeireader021)"go"2)"java"3)"python"lrangeireader0-1#-1表示最后一个1)"go"2)"java"3)"python"复制代码使用lrange获取所有元素时需要提供end_index.如果没有负下标,需要先通过llen命令获取长度,才能获取end_index的值。不用负数下标,用-1代替end_index也能达到同样的效果。修改元素使用lset命令修改指定位置的元素。>rpushireadergojavapython(integer)3lsetireader1javascriptOKlrangeireader0-11)"go"2)"javascript"3)"python"复制代码插入元素使用linsert命令在列表中间插入元素,有经验的程序大家都知道,在插入元素的时候,我们往往不知道是在指定位置之前插入还是之后插入,所以antirez在linsert命令中加入了方向参数before/after,用来显示和指示前后插入。但是让人意想不到的是,linsert命令并不是通过指定位置来插入的,而是通过指定具体的值来插入的。这是因为在分布式环境中,列表的元素总是在频繁变化,这意味着上一时刻计算的元素索引可能不是你下一时刻所期望的索引。>rpushireadergojavapython(integer)3linsertireaderbeforejavaruby??(integer)4lrangeireader0-11)"go"2)"ruby"3)"java"4)"python"在实际应用中发现插入指定的应用场景。删除元素列表的删除操作不通过指定下标来确定元素,需要指定最大删除个数和元素的值>rpushireadergojavapython(integer)3lremireader1java(integer)1lrangeireader0-11)"go"2)"python"复制代码定长列表在实际应用场景中,我们有时会遇到“定长列表”的需求。比如以走马灯的形式实时显示中奖用户名列表,因为中奖用户太多,一般能显示的数量不超过100个,那么定长列表就会在这里使用。维护一个定长列表的指令是ltrim,它需要start和end两个参数,表示需要保留列表的下标范围,范围外的所有元素都会被移除。>rpushireadergojavapythonjavascriptruby??erlangrustcpp(integer)8ltrimireader-3-1OKlrangeireader0-11)"erlang"2)"rust"3)"cpp"如果指定真实下标对应,复制代码到参数Lessthanstart的末尾,其作用等同于del命令,因为这样的参数表示列表元素的下标范围需要保持为空。如果深入quicklist就会发现,Redis的底层存储并不是简单的链表,而是一种叫做quicklist的结构。首先,当列表中的元素较少时,将使用连续的内存存储。这个结构就是ziplist,就是一个压缩列表。它将所有元素并排存储,并分配一块连续的内存。当数据量较大时,会改为quicklist。因为普通链表需要的额外指针空间太大,会造成空间浪费。比如这个链表只存放了int类型的数据,结构中还需要额外增加两个指针prev和next。所以Redis将链表和ziplist结合起来形成了quicklist。即多个ziplists使用双向指针串联使用。这样既满足了快速插入删除的性能,又不会造成过多的空间冗余。Hash相当于Java语言中的HashMap或Python语言中的dict。在实现结构上,它采用二维结构。第一维是数组,第二维是链表。哈希的内容键和值存储在链表中。数组存储链表的头指针。key查找元素时,先计算key的hashcode,然后用hashcode对数组长度取模定位链表头,然后遍历链表得到对应的值。链表的作用是将产生“哈希碰撞”的元素串在一起。Java语言开发者会觉得很熟悉,因为这样的结构和HashMap没有区别。hash的第一维数组的长度也是2^n。添加元素可以使用hset一次添加一个键值对,也可以使用hmset一次添加多个键值对>hsetireadergofast(integer)1hmsetireaderjavafastpythonslowOK复制代码得到elements可以使用hget定位到特定key对应的value,可以通过hmget获取多个key对应的value,可以使用hgetall获取所有的键值对,可以使用hkeys和hvals获取分别是所有键列表和值列表。这些操作类似于Java语言的Map接口。>hmsetireadergofastjavafastpythonslowOKhgetireadergo"fast"hmgetireadergopython1)"fast"2)"slow"hgetallireader1)"go"2)"fast"3)"java"4)"fast"5)"python"6)"slow"hkeysireader1)"go"2)"java"3)"python"hvalsireader1)"fast"2)"fast"3)"slow"复制删除元素的代码你可以使用hdel删除指定key,hdel支持同时删除多个key>hmsetireadergofastjavafastpythonslowOKhdelireadergo(integer)1hdelireaderjavapython(integer)2复制判断元素是否存在的代码通常我们使用hget获取key对应的value是否为空,直到对应的元素是否存在,但是如果value的字符串长度特别大,这样判断元素是否存在就有点浪费了。这时候可以使用hexists命令。>hmsetireadergofastjavafastpythonslowOKhexistsireadergo(integer)1copycodecounter哈希结构也可以作为一个计数器,每个内部key可以作为一个独立的计数器。如果value值不是整数,调用hincrby指令会出错。>hincrbyireadergo1(整数)1hincrbyireaderpython4(整数)4hincrbyireaderjava4(整数)4hgetallireader1)“go”2)“1”3)“python”4)“4”5)“java”6)"4"hsetireaderrustgood(integer)1hincrbyireaderrust1(error)ERRhashvalueisnotanintegerCopycodeexpansion当hash里面的元素比较拥挤(hash碰撞频繁),需要扩容。扩容需要申请一个新的双倍大小的数组,然后将所有键值对重新分配给新数组下标对应的链表(rehash)。如果哈希结构很大,比如百万级的键值对,一个完整的rehash过程会耗费很长时间。这对于单线程Redis来说有点压力。所以Redis采用了progressiverehash的方案。它会同时保留新旧哈希结构,在后续的定时任务和哈希结构的读写指令中,逐步将旧结构的元素迁移到新结构中。这样就可以避免因产能扩张而造成的线卡死。收缩Redis的哈希结构不仅会膨胀还会收缩。从这点上看,它比Java的HashMap更强大,只是扩展而已。收缩的原理与扩展相同,只是新数组的大小是旧数组的两倍。setJava程序员都知道HashSet内部实现使用的是HashMap,但是所有的值都指向同一个对象。Redis的set结构也是如此,其内部也是使用了hash结构,所有的值都指向同一个内部值。添加元素可以一次添加多个元素>saddireadergojavapython(integer)3复制读取元素的代码使用smembers列出所有元素,使用scard获取集合长度,使用srandmember获取随机count元素,如果不提供count参数,默认为1>saddireadergojavapython(integer)3smembersireader1)"java"2)"python"3)"go"scardireader(integer)3srandmemberireader"java"copycodedeleteelement使用srem删除一个或多个元素,使用spop删除一个随机元素>saddireadergojavapythonrusterlang(integer)5sremireadergojava(integer)2spopireader"erlang"复制代码判断一个元素是否存在使用sismember命令,只接收单个元素>saddireadergojavapythonrusterlang(integer)5sismemberireaderrust(integer)1sismemberireaderjavascript(integer)0复制代码sortedsetSortedSet(zset)是Redis提供的一种非常特殊的数据结构,一方面它相当于Java数据结构Map,可以为每个元素值分配一个权重分数。另一方面,它类似于TreeSet。内部元素会根据权重得分进行排序,可以得到每个元素的排名。你也可以通过得分列表的范围获取元素。zset的底层实现使用了两种数据结构,第一种是hash,第二种是跳转表。哈希的作用是将元素值与权重得分关联起来,保证元素值的唯一性。可以通过元素值找到对应的分数值。跳转列表的目的是对元素值进行排序,根据得分的范围得到元素列表。添加元素通过zadd命令可以添加一个或多个value/score对,score放在前面>zaddireader4.0python(integer)1zaddireader4.0java1.0go(integer)2复制代码length可以得到数字zset中的元素通过命令zcardNumber>zcardireader(integer)3复制代码删除元素你可以通过命令zrem删除zset中的元素,你可以删除多个>zremireadergopython(integer)2复制代码counter像哈希结构,zset也可以用作计数器。>zaddireader4.0python4.0java1.0go(integer)3zincrbyireader1.0python"5"复制代码获取排名和得分通过zscore命令获取指定元素的权重,通过zscore获取指定元素的前向排名zrank命令,通过zrevrank命令获取指定元素元素的逆序[firstlast]。正方向由小到大,负方向由大到小。>zscoreireaderpython"5"zrankireadergo#分数低的ranking测试前,rank值小(integer)0zrankireaderjava(integer)1zrankireaderpython(integer)2zrevrankireaderpython(integer)0将代码复制到根据排名区间获取元素List使用zrange命令指定排名区间参数获取对应的元素列表,同时携带withscores参数获取元素的权重。通过zrevrange命令按负排名[倒数]获取元素列表。正方向由小到大,负方向由大到小。>zrangeireader0-1#获取所有元素1)"go"2)"java"3)"python"zrangeireader0-1withscores1)"go"2)"1"3)"java"4)"4"5)"python"6)"5"zrevrangeireader0-1withscores1)"python"2)"5"3)"java"4)"4"5)"go"6)"1"复制代码scorerange获取列表通过zrangebyscore命令指定得分范围获取对应的元素列表。通过zrevrangebyscore命令获取反转元素列表。正方向由小到大,负方向由大到小。参数-inf表示负无穷大,+inf表示正无穷大。>zrangebyscoreireader051)"go"2)"java"3)"python"zrangebyscoreireader-inf+infwithscores1)"go"2)"1"3)"java"4)"4"5)"python"6)"5"zrevrangebyscoreireader+inf-infwithscores#注意正负颠倒了1)"python"2)"5"3)"java"4)"4"5)"go"6)"1"复制代码根据范围移除元素列表。可以通过排名范围或分数范围一次删除多个元素>zremrangebyrankireader01(integer)2#删除2个元素zaddireader4.0java1.0go(integer)2zremrangebyscoreireader-inf4(integer)2zrangeireader0-11)"python》复制代码jumplistzset内部的排序功能是通过“jumplist”数据结构实现的,非常特殊和复杂。读者要对本文的深入内容做好心理准备。因为zset支持随机插入和删除,所以不好用数组来表示。我们先来看一个普通的链表结构。我们需要这个链表按分值排序。这意味着当需要插入一个新的元素时,插入点需要位于特定的位置,这样链表才能继续有序。通常我们使用二分查找来寻找插入点,但是二分查找的对象必须是数组,只有数组才能支持快速location定位。链表做不到,怎么办?想想一家初创公司,一开始只有几个人,团队成员都是平等的,都是联合创始人。随着公司的壮大,人数逐渐增多,团队沟通的成本也随之增加。这时候就会引入领队制度来划分队伍。每个团队都会有一个领导者。开会时分小组,多个组长会有自己的会议安排。为了进一步扩大公司规模,还需要再增加一个级别——部门,每个部门从班组长名单中选出一名代表担任部长。部长们也将有自己的高层会议安排。跳转列表类似于这种层次结构,底层的所有元素都会串起来。然后每隔几个元素选出一个代表,再用另一层指针把这些代表串起来。然后从这些代表中挑出二级代表,再串起来。最后形成金字塔结构。想想你的家乡在世界地图上的位置:亚洲-->中国-->安徽省-->安庆市-->枞阳县-->塘沟镇-->田间村-->xxxx,也是类似的结构。“跳转列表”之所以“跳转”,是因为内部元素可能“身兼数职”。比如上图中间的元素同时处于L0、L1、L2层,可以在不同层级之间快速“跳转”。”。定位插入点时,先定位到顶层,然后下层定位,一路下潜到底部找到合适的位置,插入新的元素。你可能会问新的元素是如何插入的?插入的元素有机会“身兼多职”吗?跳转列表采用随机策略来决定一个新元素可以在哪一层兼职。首先,L0层必须是100%,L1层只有50%概率,L2层只有25%的概率,L3层只有12.5%的概率。已经随机到最上面的L31层。绝大部分元素不经过几层,只有一个能深入到顶层的元素很少,列表中的元素越多,能穿透的层级越深,能够进入顶层的概率就越大,这很公平,能不能进入中央不是靠努力,而是靠运气。