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

吃透这些Redis知识点后,面试官肯定觉得你很NB

时间:2023-03-17 17:06:27 科技观察

是数据结构而不是类型。很多文章会说redis支持5种常用数据类型,其实是一个很大的歧义。所有存储在redis中的二进制数据,其实都是一个字节数组(byte[])。这些字节数据没有数据类型。只有以合理的格式对它们进行解码后,才能将它们转换为字符串、整数或对象,才有数据类型。必须记住这一点。所以任何可以转换为字节数组(byte[])的东西都可以存储在redis中。不管你是字符串、数字、对象、图片、声音、视频、文件,把它转成字节数组就行了。所以,redis中的String并不是指的字符串,实际上代表的是最简单的数据结构,即一个key只能对应一个value。这里的key和value都是字节数组,但是key一般是字符串转换成的字节数组,value根据实际需要确定。在某些情况下,对值也有一些要求。例如,要进行自增或自减操作,该值对应的字节数组必须能够解码为数字,否则会报错。那么List这种数据结构其实就是一个key可以对应多个value,value之间是有顺序的,value值可以重复。Set的数据结构是一个key可以对应多个value,value之间没有先后顺序,value值不能重复。Hash的数据结构是指一个键可以对应多个键值对。这时候,这些键值对的顺序一般意义不大。这是一种根据名称语义而不是位置访问的数据结构。语义。SortedSet的数据结构是指一个key可以对应多个value,value按大小排序,value值不能重复。每个值都与一个称为分数的浮点数相关联。元素排序规则是:先按分数排序,再按值排序。相信现在你对这五种数据结构有了更清晰的认识,它们对应的命令对你来说就是一个小case。集群带来的问题及解决方案集群带来的好处是显而易见的,比如容量增加,处理能力增强,可以按需动态扩缩容。但同时也会引入一些新的问题,至少有以下两个。一是数据分布:存储数据时应该放在哪个节点,取数据时应该查找哪个节点。二是数据移动:集群扩容,当新增节点时,节点上的数据从哪里来;集群收缩,当节点要被淘汰时,节点上的数据到哪里去了。以上两个问题有一个共同点,就是如何描述和存储数据与节点的映射关系。而由于数据的位置是由key决定的,那么问题就变成了如何建立每个key与集群中所有节点的关联。集群的节点比较固定,数量不多,虽然有节点的增删。但是集群中存储的key是完全随机的,不规则的,不可预测的,数量庞大,非常琐碎。这就像大学与所有学生之间的关系。如果大学和学生直接挂钩,肯定会很混乱。实际情况是它们之间增加了好几层。先是院系,然后是专业,然后是年级、***、班级。经过这四层映射,关系就清晰多了。这其实是一个非常重要的结论。这个世界上没有加一层解决不了的问题。如果是这样,则添加另一层。在计算机中也是如此。Redis在数据和节点之间添加了另一层。这一层叫做slot,因为slot主要和hash有关,所以也叫hashslot。***变成了,槽放在节点上,数据放在槽里。槽解决了粒度的问题,相当于增加了粒度,方便了数据的移动。Hash解决映射问题,利用key的hash值来计算其所在的slot,方便数据分配。可以这样理解,你的学习桌上堆满了书,很乱,很难找到某本书。于是你买了几个大储物盒,把这些书按照书名的长短分别放入不同的储物盒中,然后把这些储物盒放在桌子上。就这样,桌子上多了一个储物箱,储物箱里放着书籍。这样一来,书本搬起来就很方便了,拿起一个箱子就可以了。找书也很方便,数一数书名的长度,在对应的方框里找。其实我们什么也没做,只是买了几箱,按照一定的规则把书装进了箱子里。这么简单的一招,彻底改变了原本乱七八糟的局面。是不是有点神奇?一个集群只能有16384个槽,编号为0-16383。这些槽被分配给集群中的所有主节点,分配策略不是必需的。可以指定将哪些编号的插槽分配给哪个主节点。集群记录了节点和槽的对应关系。接下来需要找到key的hash值,然后取16384的余数,余数会在几个key后落入对应的slot。slot=CRC16(key)%16384.数据以slot为单位进行移动,因为slot的数量是固定的,比较容易处理,所以解决了数据移动的问题。利用hash函数计算出key的hash值,从而可以计算出其对应的slot,然后利用slot与集群中存储的节点的映射关系,查询slot所在的节点,于是数据和节点进行映射,这样数据的分配问题就解决了。我想说的是,普通人只会学习各种技术,高手更关心的是如何跳出技术,寻求解决方案或思考方向。如果你按照这个方向走,你几乎总能找到你想要的答案。集群的命令操作选择客户端只要与集群中的一个节点建立链接,就可以获得整个集群的所有节点信息。另外,会获取所有哈希槽和节点的对应关系信息,这些信息数据会缓存在客户端,因为这些信息是很有用的。客户端可以向任意节点发送请求,那么拿到key后应该向哪个节点发送请求呢?其实把集群中key和节点的映射关系理论搬到客户端就可以了。所以客户端需要实现和集群端一样的hash函数,先计算key的hash值,然后取16384的余数,从而找到key对应的hashslot,使用缓存的slot客户端通过和节点的对应关系信息可以找到key对应的节点。然后发送请求。也可以缓存键和节点的映射关系。下次请求key时,直接获取对应的节点,无需再次计算。理论和现实总是有差距的。集群变了,客户端的缓存还没来得及更新。肯定会有key向对应的节点发送请求。事实上,密钥已经不在那个节点上了。这个节点此时应该做什么呢?这个节点可以去key实际所在的节点获取数据返回给客户端,也可以直接告诉客户端key已经不在我这里了,附上key现在所在的节点信息.让客户端再次请求,类似于HTTP302重定向。这其实是一个选择的问题,也是一个理念的问题。结果是redis集群选择了后者。因此节点只处理自己拥有的key,对于不属于自己的key即-MOVEDkey127.0.0.1:6381返回重定向错误,client再次向新节点发送请求。所以选择是一种哲学,是一种智慧。稍后会详细介绍。我们再来看另一种情况,和这个问题有些相似。Redis有一个命令可以一次取多个key,比如MGET。我称这些为多键命令。这个多键命令的请求被发送到一个节点。这里有一个潜在的问题。不知道大家有没有想过这个命令中的多个key一定是在同一个节点上?有两种情况,如果多个key不在同一个节点上,此时该节点只能返回一个重定向错误,但是多个key可能位于多个不同的节点上,此时返回的重定向错误将会很乱。所以redis集群选择不支持这种情况。如果多个key位于同一个节点上,理论上是没有问题的。redis集群是否支持取决于redis的版本。使用的时候自己测试一下就好了。在这个过程中,我们发现了一个很有意义的事情,就是非常有必要将一组相关的key映射到同一个节点,这样可以提高效率,通过multi-key命令一次获取多个值。那么问题来了,如何给这些key命名,让它们落在同一个节点上,很难先计算一个hash值,然后取余,太麻烦了。当然不是,redis已经帮我们想好了。可以简单的推理出如果两个key位于同一个节点上,那么它们的hash值一定是相同的。要具有相同的哈希值,传递给哈希函数的字符串必须相同。那么我们只能传入两个相同的字符串,那么就会变成同一个key,后面的会覆盖前面的数据。这里的问题是我们使用了整个key来计算hash值,这就导致了key和参与hash值计算的string的耦合。它们需要解耦,即key和参与计算hash值的字符串相关但不相同。基于这个原理,redis为我们提供了一种解决方案,叫做keyhashtag。先看例子,{user1000}.following,{user1000}.followers,相信大家已经看到方法了,就是只用Key中{和}之间的字符串参与hash值的计算。这样可以保证hash值相同,落在同一个节点上。但密钥不同,不会相互覆盖。使用哈希标签关联一组相关键,问题轻松愉快地解决。我相信你已经发现解决问题是巧妙和异想天开,而不是必须使用牛逼的技术和牛逼的算法。这就是小强,小而强大。***再来谈谈选择哲学。Redis的核心是以最快的速度对常用的数据结构进行key/value的访问,以及围绕这些数据结构的操作。对于那些与核心无关或者会拖累核心的,选择弱化或者不处理。这样做是为了确保核心的简单性、速度和稳定性。其实在广度和深度面前,redis选择了深度。因此,节点不处理不属于它们的密钥,并且集群不支持多密钥命令。这样一方面可以快速响应客户端,另一方面可以避免集群内部大量的数据传输和合并。单线程模型redis集群的每个节点只有一个线程,负责接受和执行所有客户端发送的请求。技术上采用了多路复用I/O,利用Linux的epoll功能,一个线程可以管理多个socket连接。另外选择单线程的原因有以下几点:1.redis是对内存的操作,速度极快(10W+QPS)2.整体时间主要消耗在网络传输上3.如果是多线程-使用线程,需要多线程同步,实现起来会变得复杂。4、线程的加锁时间甚至超过了内存操作的时间。5.频繁切换多线程上下文会消耗更多的CPU时间6,另外,单线程天然就支持原子操作,单线程的代码更容易写。事务大家都知道,就是把多个操作捆绑在一起,要么全部执行(成功),要么一个也不执行(退出去)。Redis也支持事务,但是可能和你想要的不太一样,一起来看看吧。Redis事务可以分为两步,定义事务和执行事务。使用multi命令启动一个事务,然后将所有要执行的命令依次排队。这定义了一个事务。这时使用exec命令执行事务,或者使用discard命令放弃事务。你可能希望你关心的键在你的交易开始之前不被其他人操作,那么你可以使用watch命令来监控这些键,如果这些键在开始执行之前被其他命令操作,交易将被取消.您还可以使用unwatch命令取消监视这些键。Redis事务具有以下特点:1.如果在开始执行事务之前发生错误,则所有命令都不会执行。2、一旦启动,保证所有命令一次按顺序执行,不被中断。3.如果执行过程中遇到错误,会继续执行,不会停止4.执行过程中遇到错误,不会回滚。看完这些,我很想问一句,这能叫交易吗?显然,这不是我们通常认为的事务,因为它连原子性都不能保证。不能保证原子性,因为redis不支持回滚,但是也给出了不支持的理由。不支持回滚的原因:1.Redis认为失败是命令使用不当造成的2.Redis这样做是为了保持内部实现简单快速3.Redis也认为回滚不能解决所有问题哈哈,这是Overlord条款,因此,使用redis事务的管道似乎并不多。客户端与集群的交互过程是序列化和阻塞的,即客户端发送一个命令后,必须等待响应返回,才能发送第二个命令。一对一回是一个往返时间。如果你有很多命令,如果你一条一条地执行,它会变得很慢。Redis提供了一种管道技术,允许客户端一次发送多个命令,而无需等待服务器的响应。所有命令发送完毕后,它会依次接收这些命令的所有响应。这大大节省了大量时间,提高了效率。聪明的你有没有意识到另一个问题,多个命令就是多个key,这不就是上面说的多key操作吗,那么问题来了,如何保证这些多个key在同一个节点上呢,哈哈,redis集群再次放弃了对管道的支持。但是可以在客户端模拟,即使用多个连接同时向多个节点发送命令,然后等待所有节点返回响应,然后按照命令发送的顺序排列,并将它们返回给用户代码。哦,真麻烦。协议简单了解redis协议,知道redis的数据传输格式。发送请求的协议:*numberofparametersCRLF$bytesofparameter1CRLFdataofparameter1CRLF...$bytesofparameterNCRLFdataofparameterNCRLF比如SETnamelixinjie,实际发送的数据是:*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$8\r\nlixinjie\r\n响应协议:单行回复,***byte为+错误信息,第一个字节是-整数,***字节是:bulkreply,***字节是$multiplebatchreply,***字节是*例如,+OK\r\n-ERROperationagainst\r\n:1000\r\n$6\r\nfoobar\r\n*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n可见redis的协议设计很简单。