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
