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

Redis详细介绍:5种基本数据结构_0

时间:2023-03-19 19:25:19 科技观察

1.Redis简介《Redis是一个开源(BSD许可)、内存数据结构存储,用作数据库、缓存和消息代理。——Redis是一种开源(BSD许可)内存数据结构存储,用作数据库、缓存和消息代理。(来自官网)Redis是一个开源的、高级的键值存储,是构建高性能、可扩展的Web应用程序的适用解决方案。Redis也被作者戏称为数据结构服务器,意思是用户可以通过一些命令访问一组基于简单的服务器-客户端协议的可变数据结构和TCP套接字。(Redis中使用键值对,但对应的数据结构不同。)Redis的优点以下是Redis的一些优点:速度异常快——Redis非常快,每秒可以执行大约110,000次设置(SET)操作和每秒大约81,000次读取/获取(GET)操作。支持丰富的数据类型——Redis支持开发人员常用的大多数数据类型,例如列表、集合、排序集合和哈希等等。这使得Redis很容易用来解决各种问题,因为我们知道哪些问题可以用哪些数据类型更好地解决。操作是原子的——所有Redis操作都是原子的,这确保了如果两个客户端同时访问它,Redis服务器会收到更新的值。多用途——Redis是一种多用途工具,可用于多种用例,例如:缓存、消息队列(Redis本身支持发布/订阅)、应用程序中的任何短期数据,例如Web应用程序中的会话,网页点击次数等。Redis的安装步骤相对简单。网上可以找到很多满意的教程,这里不再赘述。参考菜鸟教程的安装教程:https://www.runoob.com/redis/redis-install.html测试本地Redis的性能。安装完成后,可以先执行redis-server来启动Redis。然后运行命令redis-benchmark-n100000-q来测试同时执行100000个本地请求时的性能:当然不同电脑之间会因为各种原因会有性能差距。你可以把这个测试当成一种“乐趣”,那很好。二、Redis的五种基本数据结构Redis有五种基本数据结构,分别是:string(字符串)、list(列表)、hash(字典)、set(set)和zset(orderedset)这五个是Redis相关知识中最基础也是最重要的部分,下面结合源码和一些实践给大家讲解。1)StringstringRedis中的字符串是一个动态字符串,也就是说用户可以修改它。它的底层实现有点类似于Java中的ArrayList。有一个字符数组,从源码的sds.h/sdshdr文件中可以看到Redis底层对字符串的SDS定义,即SimpleDynamicString结构:*然而这里记录了type5SDSstrings的布局。*/struct__attribute__((__packed__))sdshdr5{unsignedcharflags;/*3lsboftype/charflagsbytedirectly。*但是这里记录了type5SDSstrings的布局。*/struct__attribute__((__packed__))sdshdr5]{unsignedcharflags;/*3lsboftype/charflagsbytedirectly.*但是这里记录了type5SDSstrings的布局。*/struct__attribute__((__packed__))sdshdr5]{unsignedcharflags;/*3lsboftype/charflagsbytedirectly__packed__))sdshdr8{uint8_tlen;/*已使用*/uint8_talloc;/*不包括标头和空终止符*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};struct__attribute__((__packed__))sdshdr16{uint16_tlen;/*used*/uint16_talloc;/*不包括标头和空终止符*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};struct__attribute__((__packed__))sdshdr32{uint32_tlen;/ull*used*/uint32_tallocating/*header*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};struct__attribute__((__packed__))sdshdr64{uint64_tlen;/*used*/uint64_talloc;/*excludingtheheaderandnullterminator*/unsignedcharflags;/*3lsboftype,5unusedbits*/charbuf[];};你会发现同一套结构在Redis中使用泛型定义了很多次,为什么不直接使用int类型呢?因为当字符串比较短的时候,len和alloc可以用byte和short来表示。为了优化内存,Redis使用不同的结构来表示SDS和C字符串的区别。为什么不考虑直接使用C语言字符串呢?因为C语言简单的字符串表示在安全性、效率、功能性等方面不符合Redis对字符串的要求。我们知道C语言用一个长度为N+1的字符数组来表示一个长度为N的字符串,而字符数组的最后一个元素永远是'\0'。(下图是C语言中取值为“Redis”的字符数组)这样简单的数据结构可能会导致以下问题:获取字符串长度的操作是O(N)级别的→因为C不保存数组Length,每次都需要遍历整个数组;bufferoverflow/memoryleak的问题不能很好的预防→和上面问题的原因一样,如果你进行拼接或者缩短字符串的操作,如果操作不当,很容易导致上面的问题;C字符串只能保存文本数据→因为C语言中的字符串必须符合一定的编码(如ASCII),例如中间出现的'\0'可能会被判断为过早终止的字符串而无法识别;我们以追加字符串的操作为例,Redis源码如下:有效并且所有的*引用都必须用调用返回的新指针替换。*/sdsssdscatlen(stds*s,){//获取原始字符串的长度size_tcurlen=sdslen(s);//调整空间为需要时,如果容量不足以容纳额外的内容,它将重新分配字节数组并将原始字符串的内容复制到新数组s=sdsMakeRoomFor(s,len);if(s==NULL)returnNULL;//内存不足memcpy(s+curlen,t,len);//追加目标字符串到字节数组sdssetlen(s,curlen+len);//设置追加后的长度s[curlen+len]='\0';//让字符串以\0结尾,方便调试和打印返回;}注意:Redis规定字符串长度不能超过512MB。对字符串的基本操作安装好Redis后,我们就可以使用redis-cli在命令行中对Redis进行操作了。当然,Redis官方提供了在线调试器,你也可以在里面输入命令进行操作:http://try.redis.io/#run设置并获取键值对>SETkeyvalueOK>GETkey"value"如您所见,我们通常使用SET和GET来设置和获取字符串值。值可以是任何类型的字符串(包括二进制数据),例如您可以在键下存储.jpeg图像,注意不要超过512MB的最大值。当key存在时,SET命令会覆盖你上次设置的值:>SETkeynewValueOK>GETkey"newValue"另外,你还可以使用EXISTS和DEL关键字查询键值对是否存在并删除:>EXISTSkey(integer)1>DELkey(integer)1>GETkey(nil)批量设置键值对>SETkey1value1OK>SETkey2value2OK>MGETkey1key2key3#returnalist1)"value1"2)"value2"3)(nil)>MSETkey1value1key2value2>MGETkey1key21)"value1"2)"value2"expiration和SET命令扩展可以为key设置过期时间,时间到会自动删除。该函数常用于控制缓存的过期时间。(过期可以是任何数据结构)>SETkeyvalue1>GETkey"value1">EXPIREname5#Expiresafter5s...#Wait5s>GETkey(nil)相当于SET+EXPIRE的SETNX命令:>SETNXkeyvalue1...#Wait5sGetlater>GETkey(nil)>SETNXkeyvalue1#如果key不存在,SET成功(整数)1>SETNXkeyvalue1#如果key存在,SET失败(整数)0>GETkey"value"#Nochangecount如果value是整数,仍然可以使用INCR命令进行原子自增操作,也就是说即使多个客户端对同一个key进行操作,也绝不会造成竞争:>SETcounter100>INCRcount(interger)101>INCRBYcounter50(integer)151GETSET返回原值的命令字符串,还有一个比较有意思的GETSET,它的作用和它的名字一样:给key设置一个值,返回原值:>SETkeyvalue>GETSETkeyvalue1"value"这个可以用对于某些需要定时统计的key,设置和查看都很方便,例如:whenever系统有用户访问,你用INCR命令操作一个key,需要统计的时??候,用GETSET命令重新赋值key为0,达到统计的目的。2)List列表Redis列表相当于Java语言中的LinkedList,注意是链表而不是数组。这意味着列表的插入和删除操作非常快,O(1)时间复杂度,但是索引定位很慢,O(n)时间复杂度。从源码的adlist.h/listNode可以看到它的定义:/*Node,List,Iterator是目前唯一使用的数据结构。intdirection;}listIter;typedefstructlist{listNode*head;listNode*tail;void*(*dup)(void*ptr);void(*free)(void*ptr);int(*match)(void*ptr,void*钥匙);无符号长号;}列表;可以看出,多个listNode可以通过prev和next指针组成双向链表:虽然只能用多个listNode结构组成链表,但是adlist.h/list结构是用来存放链表的操作起来会更方便:链表LPUSH和RPUSH的基本操作可以分别在链表的左边(头)和右边(尾)添加一个新元素;LRANGE命令可以从列表中提取一定范围内的元素;LINDEX命令可以从列表中取出下面列表中指定的元素,相当于Java链表操作中的get(intindex)操作;演示:>rpushmylistA(integer)1>rpushmylistB(integer)2>lpushmylistfirst(integer)3>lrangemylist0-1#-1表示第一个最后一个元素,这里表示从第一个元素到最后一个元素,即全1)"first"2)"A"3)"B"list实现队列队列是一种先进先出的数据结构,常用于消息队列和异步逻辑处理,它会保证元素的访问顺序:>RPUSHbookspythonjavagolang(整数)3>LPOPbooks"python">LPOPbooks"java">LPOPbooks"golang">LPOPbooks(nil)list栈的实现是先进后出的数据结构,正好和队列相反:>RPUSHbookspythonjavagolang>RPOPbooks"golang">RPOPbooks"java">RPOPbooks"python">RPOPbooks(nil)3)hashRedis中的字典相当于Java中的HashMap,其内部实现也差不多。它使用“数组+链表”的链地址方式来解决一些哈希冲突问题。同时,这种结构也吸收了两种不同数据结构的优点源码定义如dict.h/dicttht定义:typedefstructdict{//hashtablearraydictEntry**table;//hashtablesizeunsignedlongsize;//hashtablesizemask,用于计算索引值,始终等于size-1unsignedlongsizemask;//哈希表中已有的节点数unsignedlongused;}dicttht;typedefstructdict{dictType*type;void*privdata;//里面有两个dicttht结构dicththt[2];longrehashidx;/*rehashingnotinprogressifrehashidx==-1*/unsignedlongiterators;/*numberofiteratorscurrentlyrunning*/}dict;table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存一个键值对:typedefstructdictEntry{//keyvoid*key;//valueunion{void*val;uint64_tu64;int64_ts64;doubled;}v;//指向下一个哈希表节点组成链表structdictEntry*next;}dictEntry;可以从上面的源码中看到,字典结构其实包含了两个哈希表。通常,只有一个哈希表具有价值。但是,当字典扩容和缩容时,需要分配一个新的哈希表,然后逐渐搬迁(原因在下面解释)。progressiverehash大字典的扩容比较耗时。需要重新申请一个新数组,然后将旧字典的所有链表中的元素重新附加到新数组中。这是一个O(n)级别的操作,单线程的Redis难以承受这么耗时的过程,所以Redis使用progressiverehash来小步搬迁:progressiverehash会同时保留新旧hash结构如rehash,如上图所示,在查询时,会同时查询一个hash结构,然后在后续的定时任务和hash操作指令中,逐步将旧字典的内容迁移到新字典中。当迁移完成后,将使用一个新的哈希结构来替换它。扩容和缩容的条件一般情况下,当哈希表中的元素个数等于一维数组的长度时,就会开始扩容,新的扩容后的数组会是原数组大小的两倍。但是,如果Redis在做bgsave(持久化命令),为了减少内存,就得分开太多。Redis尽量不扩容,但是如果哈希表很满,达到第一维数组长度的5倍,这时候就会强制扩容。当哈希表由于元素的逐渐删除而变得稀疏时,Redis会收缩哈希表以减少哈希表第一维数组占用的空间。使用的条件是元素个数小于数组长度的10%,缩放不考虑Redis是否在做bgsave。字典的基本运算散列也有缺点。hash结构的存储消耗高于单个字符串,所以是使用hash还是string需要根据实际情况权衡:>HSETbooksjava"thinkinjava"#如果命令行字符串中包含空格则需要使用引号包裹(整数)1>HSETbookspython"pythoncookbook"(整数)1>HGETALLbooks#键和值间隔出现1)"java"2)"thinkinjava"3)"python"4)"pythoncookbook">HGETbooksjava"thinkinjava">HSETbooksjava"headfirstjava"(integer)0#因为是更新操作,所以返回0>HMSETbooksjava"effectivejava"python"learningpython"#批量操作OK4)setRedis的集合相当于Java语言中的HashSet,以及它的内部键值对是无序且唯一的。它的内部实现相当于一个特殊的字典,字典中的所有值都是一个值NULL。collectionset的基本使用由于结构比较简单,直接看下如何使用:>SADDbooksjava(integer)1>SADDbooksjava#Repeat(integer)0>SADDbookspythongolang(integer)2>SMEMBERSbooks#注意toorder,setisnoneSequential1)"java"2)"python"3)"golang">SISMEMBERbooksjava#查询某个值是否存在,相当于contains(integer)1>SCARDbooks#getlength(integer)3>SPOPbooks#弹出一个“java”5)有序列表zset这可能是Redis最具特色的数据结构了。类似于Java中SortedSet和HashMap的结合。一方面,它是一个集合,保证了内部值的唯一性。另一方面,它可以为每个值分配一个分值,用于表示排序权重。它的内部实现使用了一种叫做“跳表”的数据结构。因为比较复杂,这里简单提一下原理就够了:假设你是一家创业公司的老板。一开始只有少数人,每个人都是平等的。后来随着公司的发展,人数增多,团队沟通的成本也逐渐增加。逐渐引入组长制,把团队划分开来,有些人既是员工又是组长。后来公司规模进一步扩大,公司需要进入另一个层次:部门。因此,各部门会从组长中选出一名部长。跳转表类似于这种机制。底层的所有元素都会连接在一起,都是员工,然后每隔几个元素选出一个代表,再用另一层指针连接这些代表。.然后在这些代表中挑出二级代表,然后串起来。最终形成金字塔结构。想想你现在的地理位置:亚洲>中国>某省>某市>....就是这样的结构!有序列表zset基本操作>ZADDbooks9.0"thinkinjava">ZADDbooks8.9"javaconcurrency">ZADDbooks8.6"javacookbook">ZRANGEbooks0-1#按分数排序,参数interval为排名范围1)"javacookbook"2)"javaconcurrency"3)"thinkinjava">ZREVRANGEbooks0-1#按照分数倒序排列,参数区间为排名区间1)"thinkinjava"2)"javaconcurrency"3)"javacookbook">ZCARDbooks#相当于count()(integer)3>ZSCOREbooks"javaconcurrency"#获取指定值的分数"8.9000000000000004"#内部分数存储为double类型,所以存在小数点精度问题>ZRANKbooks"javaconcurrency"#Rank(integer)1>ZRANGEBYSCOREbooks08.91#根据分数区间zset1)"javacookbook"2)"javaconcurrency">ZRANGEBYSCOREbooks-inf8.91withscores#根据分数区间(-∞,8.91]遍历zset,返回分数。inf代表无穷大,这意味着无限。1)"javacookbook"2)"8.5999999999999996"3)"javaconcurrency"4)"8.9000000000000004">ZREMbooks"javaconcurrency"#删减值(整数)1>ZRANGEbooks0-11)"javacookbook"2)"thinkinjava"