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

大厂经典面试题:为什么Redis这么快?

时间:2023-03-16 15:03:10 科技观察

前言大家好,我是捡蜗牛的小男孩。我们都知道Redis的速度非常快,它的QPS可以达到10万(每秒请求数)。为什么Redis这么快?本文将与您一起学习。基于内存实现,我们都知道内存读写比磁盘读写快很多。Redis是基于内存存储实现的数据库。与数据存储在磁盘上的数据库相比,节省了磁盘I/O的消耗。MySQL等磁盘数据库需要建立索引来加快查询效率,而Redis数据存储在内存中,直接操作内存,所以速度非常快。高效的数据结构我们知道,为了提高效率,MySQL索引选择了B+树的数据结构。事实上,合理的数据结构可以让你的应用程序/程序更快。先看Redis的数据结构&内部编码图:SDSsimpledynamicstringstructsdshdr{//SDSsimpledynamicstringintlen;//记录buf中使用的空间intfree;//buf中空闲空间的长度charbuf[];//实际存储的内容}字符串长度处理C语言中,要得到捡蜗牛小男孩的字符串长度,需要从头开始遍历,复杂度为O(n);在Redis中,已经有一个len字段记录了当前字符串的长度,可以直接获取,时间复杂度为O(1)。减少重新分配内存的次数在C语言中,修改一个字符串需要重新分配内存。修改越频繁,内存分配就越频繁,分配内存会消耗性能。在Redis中,SDS提供了两种优化策略:空间预分配和惰性空间释放。空间预分配SDS在简单修改动态字符串和扩展空间时,除了分配必要的内存空间外,还会分配额外未使用的空间。分配规则很简单:修改SDS后,如果len的长度小于1M,则额外分配一个与len长度相同的未使用空间。比如len=100,重新分配后,buf的实际长度会变成100(已用空间)+100(多余空间)+1(空字符)=201。SDS修改后,如果len的长度大于1M,程序会分配1M的未使用空间。Lazyspacerelease当SDS被缩短时,并不会回收多余的内存空间,而是使用free来记录多余的空间。后续的修改操作会直接使用free中的空间,减少内存分配。HashRedis是一个K-V内存数据库,它使用全局哈希来保存所有的键值对。这个哈希表由多个哈希桶组成。哈希桶中的entry元素存储*key和*value指针,其中*key指向实际的key,*value指向实际的value。哈希表的查找速度非常快,有点类似于Java中的HashMap,可以让我们在O(1)的时间复杂度内快速找到键值对。先通过key计算hash值,找到对应的hashbucket位置,然后定位到entry,在entry中找到对应的数据。可能有朋友会有疑惑:当你向哈希表写入大量数据时,会遇到哈希冲突的问题,效率会下降。哈希冲突:通过不同的key,计算出相同的哈希值,导致落入同一个哈希桶。”Redis为了解决哈希冲突而使用了链式哈希。链式哈希是指在同一个哈希桶中,多个元素存储在一个链表中,依次用指针连接起来。可能有的小伙伴也会有疑惑:哈希冲突链上的元素只能通过指针逐个查找和操作,当向哈希表插入大量数据时,冲突会更多,冲突列表会越长,查询效率会越高为了保持高效率,Redis会对哈希表进行rehash,即增加哈希桶来减少冲突。为了让rehash更高效,Redis默认还使用了两张全局哈希表,一张用于当前使用,称为主哈希表,一个用于扩展,称为备用哈希表。跳表性示意图如下:每一层都有一个有序的链表,最底层的链表包含了所有的元素。跳表支持平均O(logN)、最差O(N)复杂度的节点搜索,也可以通过顺序批处理节点进行性操作。CompressedlistziplistCompressedlistziplist是列表键和字典键的底层实现之一。它是由一系列经过特殊编码的内存块组成的列表。一个ziplist可以包含多个entry,每个entry可以保存一个有长度限制的字符数组或整数,如下:zlbytes:记录整个压缩列表占用的内存字节数zltail:尾节点到起始节点的偏移量zllen:记录整个压缩列表包含的节点个数entryX:压缩列表包含的每个节点zlend:特殊值0xFF(十进制255),用于标记压缩列表结束由于内存是连续分配的,所以遍历速度非常快。.合理的数据编码Redis支持多种基本数据类型,每种数据类型对应不同的数据结构,每种数据结构对应不同的编码。为了提高性能,Redis的设计者总结出这种数据结构是最适合的编码搭配。Redis使用对象(redisObject)来表示数据库中的键值。当我们在Redis中创建键值对时,至少会创建两个对象,一个对象是作为键值对的键对象,另一个是键值对的值对象。//重点关注公众号:捡蜗牛的小男孩typedefstructredisObject{//typeunsignedtype:4;//编码unsignedencoding:4;//指向底层数据结构的指针void*ptr;//...}robj;inredisObject,type对应对象类型,包括String对象、List对象、Hash对象、Set对象、zset对象。编码对应于编码。String:如果存储数字,使用int类型编码;如果存储的是非数字,小于等于39字节的字符串,就是embstr;如果大于39字节,则为原始编码。List:如果列表中的元素个数小于512,则列表中每个元素的值小于64字节(默认),使用ziplist编码,否则使用linkedlist编码hash:hash类型元素个数较少大于512,所有值都小于64字节,使用ziplist编码,否则使用hashtable编码。Set:如果集合中的元素都是整数且元素个数小于512,则使用intset编码,否则使用hashtable编码。zset:当有序集合的元素个数小于128且每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳表)编码合理的线程模型单线程模型:避免上下文切换Redis它是单线程的。其实就是说Redis的网络IO和key-value对的读写都是一个线程完成的。但是Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实都是通过额外的线程来执行的。Redis的单线程模型避免了CPU不必要的上下文切换和竞争锁的消耗。也因为是单线程,如果一个命令执行的时间过长(比如hgetall命令),也会造成阻塞。Redis是内存数据库,适用于快速执行场景,因此lrange、smembers、hgetall等命令需谨慎使用。什么是上下文切换?我举个例子:比如你正在看一本英文小说。当你看到某一页时,你发现有一个字你看不懂。您添加一个书签,然后在字典中查找它。查完字典,回来从书签开始看,这个过程很惬意。如果你一个人读这本书,你会没事的。但是如果你去查字典,其他朋友翻翻你的书就溜走了。回来再看的时候,发现书页不是你看的那一页,还得花时间去找你的那一页。一本书,单看怎么看、贴什么标签都无所谓,但如果翻阅的人太多,这本书的各种标签就会很乱。也许这个解释很粗略,但道理应该是一样的。I/O多路复用什么是I/O多路复用?I/O:网络I/O多路复用:多个网络连接多路复用:多路复用同一个线程。IO多路复用实际上是一种同步IO模型,可以让一个线程监听多个文件句柄;一旦文件句柄就绪,就可以通知应用程序进行相应的读写操作;当没有文件句柄准备好时,它会阻塞应用程序并交出cpu。多I/O多路复用技术允许单个线程高效处理多个连接请求,Redis使用epoll作为I/O多路复用技术的实现。并且Redis自带的事件处理模型将epoll中的连接、读写、关闭都转化为事件,以免在网络I/O上浪费太多时间。《虚拟内存机制》Redis直接自己搭建了VM机制,不会像一般系统那样调用系统函数处理,会浪费一定的时间去移动和请求。Redis的虚拟内存机制是什么?经常访问数据(冷数据)从内存交换到磁盘,从而为其他需要访问的数据(热数据)腾出宝贵的内存空间,通过VM功能可以实现冷热数据的分离,让热数据内存中保留的中间数据和冷数据保存到磁盘,这样就可以避免内存不足导致访问速度变慢的问题。参考[1]Redis的VM机制:https://www.codenong.com/cs106843764/[2]揭秘单线程Redis为什么这么快?:https://zhuanlan.zhihu.com/p/57089960[3]洞察|Redis是单线程的,但是Redis为什么这么快?:https://zhuanlan.zhihu.com/p/42272979