本文转载自微信公众号《IT行业的打工仔》,作者LeoWu。转载本文请联系IT行业农民工公众号。作为一名服务器工程师,你在工作中肯定和Redis打过交道。你必须知道Redis为什么快,至少你已经为面试做好了准备。很多人知道Redis的速度快只是因为它是基于内存实现的,但是对于其他的原因却模棱两可。那么今天就和小赖一起来看看吧:-思维导图-基于记忆的实现这一点在开头就提到了,这里简单说一下。Redis是一个基于内存的数据库,所以难免要和磁盘数据库进行类比。对于磁盘数据库来说,需要将数据读入内存,这个过程会受到磁盘I/O的限制。对于内存数据库来说,数据本身就存在于内存中,所以没有这样的开销。高效的数据结构Redis中有多种数据类型,每种数据类型的底层由一种或多种数据结构支撑。正是因为有了这些数据结构,Redis在存储和读取上的速度才没有受到阻碍。这些数据结构有什么特点呢?下面我们看一下:1、简单动态字符串这个词你可能不熟悉,但是换成SDS你就知道了。这是为了处理字符串。了解C语言的人都知道它有处理字符串的方法。而Redis是用C语言实现的,为什么要重新造轮子呢?我们来看以下几点:(1)字符串长度处理这张图展示了字符串在C语言中是如何存储的。如果想得到"Redis"的长度,需要从头开始遍历,直到遇到'\0'。在Redis中如何实现?使用len字段记录当前字符串的长度。获取长度只需要获取len字段即可。你看,差距不言而喻。前者遍历的时间复杂度为O(n),在Redis中可以得到O(1),速度明显提升。(2)内存重新分配C语言中修改字符串时,会重新分配内存。修改越频繁,内存分配就越频繁。但是内存分配是消耗性能的,所以性能下降在所难免。但是Redis会涉及到频繁的字符串修改操作,这种内存分配方式显然不适合。因此,SDS实现了两种优化策略:空间预分配在修改和扩展SDS时,除了分配必要的空间外,还会分配额外的未使用空间。具体分配规则如下:修改SDS后,如果len长度小于1M,则额外分配一个与len长度相同的未使用空间。如果修改后的长度大于1M,则分配1M的使用空间。惰性空间释放当然也有空间分配对应的空间释放。当SDS被缩短时,并不会回收多余的内存空间,而是使用free域来记录多余的空间。如果后续有change操作,则直接使用free中记录的空间,减少内存分配。(3)二进制安全你已经知道Redis可以存储各种数据类型,二进制数据当然也不例外。但是二进制数据不是常规的字符串格式,可能包含一些特殊字符,如'\0'等。前面我们说过,C中的字符串遇到'\0'就会结束,'\0'之后的数据是读不到的。但是在SDS中,字符串的结尾是根据len的长度来判断的。看,二进制安全问题解决了。2.双端链表List更多的是作为队列或者栈来使用。队列和栈的特点是先进先出和先进后出。双端链表很好地支持这些特性。-双端链表-(1)前后节点链表中的每个节点都有两个指针,prev指向上一个节点,next指向下一个节点。这样就可以在O(1)的时间复杂度内得到前后节点。(2)头节点和尾节点大家可能注意到了,头节点中有两个参数head和tail,分别指向头节点和尾节点。这样的设计可以将双端节点的处理时间复杂度降低到O(1),非常适合队列和栈。同时可以从两端迭代链表。(3)链表的长度在头节点中还有一个参数len,和上面说的SDS类似,用来记录链表的长度。因此,在获取链表长度时,不需要遍历整个链表,直接获取len值即可。这个时间复杂度是O(1)。你看,这些特性减少了使用List时的时间开销。3、压缩列表双端链表我们已经很熟悉了。不知道大家有没有注意到一个问题:如果在一个链表节点中存储一个小数据,比如一个字节。那么相应的头结点、前后指针等附加数据都要保存。这样既浪费空间,又容易因反复申请和释放而造成内存碎片。这个内存使用效率太低了。所以,压缩列表开始发挥作用了!它经过特殊编码和设计以提高内存使用效率。所有操作都是通过指针和解码后的偏移量来执行的。而且压缩列表的内存是连续分配的,遍历速度非常快。4.字典Redis是一个K-V数据库,所有的键值都存储在字典中。你应该熟悉日常学习中使用的词典。想找某个词,直接通过某个词定位即可,速度非常快。这里说的字典基本都是一样的,直接通过某个key就可以得到对应的值。字典也叫哈希表,这个没啥好说的。哈希表的特性众所周知,关联值的检索和插入的时间复杂度为O(1)。5、跳表是Redis独有的一种数据结构——跳表,在链表的基础上增加了多级索引,提高了查找效率。这是跳表的简单示意图。每一层都有一个有序的链表,最底层的链表包含了所有的元素。这样跳表就可以支持在O(logN)的时间复杂度内找到对应的节点。下面是跳表的真实存储结构。和其他数据结构一样,在头节点记录相应的信息,减少了一些不必要的系统开销。合理的数据编码对于每一种数据类型,底层支持的可能是多种数据结构。什么时候使用哪种数据结构,这就涉及到编码转换的问题。那么我们来看看不同数据类型是如何编码和转换的:String:如果存储的是数字,就使用int类型的编码;如果不是数字,则使用原始编码;List:字符串的长度和元素个数小于一定范围使用ziplist编码,不满足任何条件都会转成linkedlist编码;哈希:哈希对象中存储的键值对中键值字符串的长度小于某个值和键值对;set:将元素保存为整数,元素编号如果小于一定范围,则使用intset编码;如果不满足任何条件,则使用哈希表编码;zset:如果zset对象中存储的元素个数小于一定值且成员长度小于一定值,则使用ziplist编码;如果不满足任何条件,则使用跳过列表编码。合适的线程模型Redis之所以快,另一个原因是它使用了合适的线程模型:1.I/O多路复用模型I/O:网络I/O多路复用:多个TCP连接多路复用:共享一个线程或进程中的使用生产环境通常会有多个客户端连接Redis,然后向Redis服务器发送命令,最后由服务器处理这些请求并返回结果。为了处理大量请求,Redis使用I/O多路复用器同时监听多个套接字,并将这些事件压入一个队列,然后一个一个地执行。最后将结果返回给客户端。2.避免上下文切换你一定听说过Redis是单线程的。那么单线程Redis为什么快呢?因为多线程在执行过程中需要进行CPU上下文切换,而这个操作是比较耗时的。Redis是基于内存实现的。对于内存来说,无上下文切换的效率是最高的。在一个CPU上进行多次读写,是内存的最佳方案。3.单线程模型顺便说一下为什么Redis是单线程的。Redis使用的是Reactor单线程模型,大家可能不太熟悉。没关系,你只需要了解一个大概的概念。这张图中,接收到用户的请求后,全部推入一个队列,然后交给文件事件派发器,是一种单线程的工作方式。Redis基于它工作,所以Redis是单线程的。总结基于内存实现,所有数据都存储在内存中,减少了一些不必要的I/O操作,运行速度非常快。高效的数据结构多种底层数据结构,支持不同的数据类型,支持Redis存储不同的数据;不同数据结构的设计最大限度地降低了数据存储的时间复杂度。合理的数据编码根据字符串的长度和元素个数,适配不同的编码格式。适当的线程模型I/O多路复用模型同时监听客户端连接;单线程执行时不需要上下文切换,减少耗时。
