【说透Redis】为什么单线程的Redis那么快?

1,633 阅读7分钟

我们都知道Redis很快,我们还总是听别人说Redis是单线程的,那么单线程的Redis为什么那么快呢?

1. Redis单线程的本质

其实,Redis并不是单线程,我们之所以会一直称Redis是单线程,这是因为Redis在处理客户端的读写请求时,只有一个主线程,而在处理以下这些操作时,Redisfork出其他的子线程来处理:

  • 主从数据同步
  • 切片集群数据同步
  • 过期键值异步删除
  • AOF或RDB持久化

所以整体来看Redis并不是单线程。

Redis6.0中引入的多线程机制,实际上只是将网络IO读写处理这块逻辑变成多线程,因为在Redis6.0以前的版本中, 网络IO请求也是在主线程中进行处理,随着互联网应用的高并发访问以及网络硬件性能的提升,在主线程中进行网络IO处理已经成为Redis的瓶颈,因此使用多线程来处理网络IO请求,可以显示提高Redis响应速度,而对于键值对的读写,仍然由主线程一个线程进行处理,这样可以仍然可以不用加锁也能保证读写操作的原子性,避免多线程互斥机制带来的性能损耗。

2. 单线程为什么那么快?

通过前面的介绍,我们知道,Redis并非完全是单线程,但在处理网络IO和数据读写等Redis核心功能时,Redis确实是由主线程处理的,那么我们就不禁有个疑问,Redis的单线程为什么那么快呢?

总结了以下大概有以下四个方面的原因:

  • 内存操作
  • 高效的底层数据结构
  • 多路复用IO模型
  • 避免多线程切换开销

2.1 内存操作

Redis是内存数据库,读写操作都在内存中,学过计算机基础的我们都知道,CPU读取内存的速度要比读取磁盘的速度快得多,所以单台Redis服务器每秒能处理数十万的读取也就不足为怪了,事实上,基于内存的读写操作,是Redis能这么快的最重要的原因。

4.png

2.2 高效的底层数据结构

我们都知道Redis提供了五种非常好用的数据类型:StringListHashSetSorted Set

Redis有六种底层数据结构,分别为哈希表,压缩列表,跳表,整数数组,简单动态字符串。

数据类型与其底层数据结构对应关系如下图所示

5.png

总结来说就是:

  • String:Redis没有使用C语言内置的字符数组,而是将字符数组封装为简单动态字符串(SDS),虽然SDS比C语言原生字符数组更费内存,但通过空间换时间,可以将很多C语言字符数组时间复杂度为O(n)的操作转为O(1),提高处理速度,比如strlen命令获取String长度时,对于C语言的字符数组,需要遍历数组才能知道,时间复杂度为O(n),而简单动态字符串已经保存了字符串的长度,因此可以直接获取,时间复杂度为O(1)。
  • Hash:在数据量还不多的情况下,Hash类型使用压缩列表(ZipList)保存元素,因为元素不多,所以查找也比较快,如果数据增长到设定的值,就改为哈希表,而哈希表查找元素的时间复杂度为O(1),所以也是相当快的。
  • List:List的底层数据结构为双向链表和压缩列表组合而成的,也称为快速链表(QuickList),因此List类型非常适合头尾插入弹出的操作,因此如果要把Redis作为队列的话,选择List是非常高效的。
  • Sorted Set:当元素数量不多时,Sorted Set使用的是压缩列表,只有当元素超过设定值时,才使用跳表和哈希表。
  • Set:在元素的数值都是整数时,Set使用整数数组保存数据,如果元素数量达到设置的值,则改为哈希表。

Redis是一个Key-Value健值对数据库,我们上面介绍的五种类型是指Key-Value中的Value,对于所有KeyValue之间的映射,Redis也是采用哈希表,这个哈希表也叫全局哈希表,如下图所示:

6.png

2.3 多路复用IO模型

Redis高性能的另一个重要的原因是采用多路复用IO模型,由于Redis是单线程处理网络请求的,如果采用阻塞IO模型,那么对于每一个请求,Redis需要从接收连接->读取连接数据->处理命令->返回数据整个流程处理完成后才能处理处理下一个请求,如果是这样的话,那么Redis的快就无从谈起了。

而采用非阻塞模型IO,虽然可以避免阻塞,但这种模型会在内核空间与用户空间来回复制全部要监听的FD(文件描述符),除了之外,我们还无法得知哪个FD已经就绪了,需要遍历所有的FD才能知道,同样是即费空间又费时间,因此对于需要支持高并发的Redis来说,显然也是不可接受的。

多路复用IO模型则不同了,多路复用IO模型同样也是非阻塞的,其内置的红黑树可以高效地添加或查找FD,且只会将已经就绪的的FD从内核空间复制到用户空间,因此很多网络应用处理请求时都是使用多路复用IO模型,Redis也不例外。

多路复用IO模型在不同的操作系统有不同的实现,如selec和poll,还有更性能更好的epoll,Redis不同操作系统的多路复用IO模型都有封装。

2.4 避免多线程切换

因为将数据保存在内存中,并且有高效的数据结构和使用多路利用IO模型,所以Redis读取数据非常快速,这时候,你可能会想,是不是将Redis改为多线程,可以更好提升Redis的吞吐量,处理更多并发请求呢?

其实不然,正因为Redis读取非常快,所以如果采用多线程的话,会产生以下两个问题:

  • 多线程切换带来的额外开销:因为Redis读写非常快速,因为如果采用多线程,那么线程切换就非常频繁,所以如果采用多线程的话,Redis大部分时间可能都在切换线程。
  • 数据加锁的性能损耗:多线程访问同一个数据的话,为了避免竞争,肯定需要加锁保证数据操作的原子性,而加锁与等待锁的释放,让多个线程在读取同一个数据需要排队等待,所以效率并不比单线程强多少。

3 怎么让Redis更快

单台Redis服务器,已经非常快了,那么,有什么办法能让Redis更快呢?

  • 使用AOF或RDB数据备份:有了数据备份,可以在服务器宕机时,更快地恢复
  • 主从分离:让从服务器分担主服务器的压力,实现更快地响应
  • 切片集群:除了主从,切片集群让数据分散到不同服务器,多台服务器可分担读写的压力
  • 避免bigkey:bigkey是指键值过大或者集合元素过多,占用太多的带宽与CPU运算,导致主线程阻塞,所以要避免在Redis存储比较大的值,比如对于value来说,不要超过10KB,对于集合元素,元素的数量最好不要超过1000

4 小结

从上面的分析我们可以看,Redis并不是像我们认为的是单线程,在处理持久化等耗时任务时,Redis也是采用多线程的处理方式,不过,Redis在处理请求时只有一个主线程,但仍然做非常快速的响应,这是由于Redis的数据读写都在内存中,而内存的访问是非常快速的,另外Redis为每一种数据类型都精心设计了高效的底层数据结构,而在处理网络请求时,则采用基于多路复用的IO模型,使得Redis可以高并地处理更多的请求。