文章摘要

这篇文章详细介绍了Redis的高性能原因。内容包括以下几个方面:

1. 内存存储:Redis将数据存储在内存中,避免了磁盘I/O操作,从而大大提高了数据读写速度。
2. 单线程模型:Redis采用单线程模型,避免了多线程的上下文切换和竞争问题,使得操作更加高效。
3. 高效的数据结构:Redis使用了高效的数据结构,如字符串、哈希、列表、集合和有序集合,优化了数据存取的效率。
4. I/O多路复用:Redis使用I/O多路复用技术,通过一个线程处理多个连接,提高了并发处理能力。
5. 优化的序列化协议:Redis使用RESP(REdis Serialization Protocol)协议,简化了客户端和服务器之间的数据传输。

文章通过详细的解释和实例,帮助读者理解Redis为何能够实现高性能。整体内容适合对Redis性能感兴趣的初学者和有一定基础的用户参考学习。

前言

Redis 是一个开源的内存数据结构存储,广泛用于缓存、消息队列和实时数据处理等场景。Redis 之所以能够实现高性能和低延迟,主要归功于其设计和实现中的一些关键技术和优化策略。

Redis以性能著称,很快,到底有多快呢,我们来看一下官网提供的数据:

QPS可以达到100000,是什么原因让Redis这么快?主要有以下四点:

  • Redis是基于内存的数据库
  • IO多路复用
  • 高效的数据结构
  • 单线程模型

基于内存

Redis是基于内存的数据库,这样就减少了不必要的磁盘IO操作,大大提升了读写速度,下图是各种介质的处理时间

由上图可见,内存的存取时间大约为120纳秒,而硬盘的存取时间最快也要50微秒,换算下来,内存的速度至少是硬盘的400多倍。注:1ms=1000us,1us=1000ns,ms表示毫秒,us表示微秒,ns为纳秒。

IO多路复用

Redis 的 IO 多路复用(IO Multiplexing)是指在一个单线程内同时处理多个网络连接的技术。通过这种技术,Redis 能够高效地处理大量并发连接,从而实现高性能和低延迟。

IO 多路复用是一种操作系统提供的机制,它允许一个单线程在等待多个文件描述符(如网络连接、文件等)上的事件时,不会阻塞。这种技术使得单个线程可以同时处理多个网络连接,而不需要为每个连接创建一个线程。

Redis 使用以下几种系统调用来实现 IO 多路复用:

evport:Solaris 的 IO 多路复用机制。

select:最早的 IO 多路复用机制,适用于几乎所有的操作系统,但在处理大量文件描述符时性能较差。

poll:改进版的 select,没有文件描述符数量限制,但在性能上仍然有一些不足。

epoll:Linux 特有的 IO 多路复用机制,性能优越,适用于大规模并发连接。

kqueue:FreeBSD、OpenBSD、NetBSD 以及 macOS 的 IO 多路复用机制,性能优越。

在 Redis 启动时,它会根据操作系统的类型和可用性,自动选择最佳的 IO 多路复用机制。

IO 多路复用的工作原理

Redis 的 IO 多路复用机制主要包括以下步骤:

  1. 初始化:在 Redis 启动时,初始化 IO 多路复用机制,创建事件循环。
  2. 注册事件:将需要监听的文件描述符(如客户端连接的套接字)注册到 IO 多路复用机制中。
  3. 等待事件:使用 IO 多路复用系统调用(如 epoll_wait)等待文件描述符上的事件(如读、写、错误等)。
  4. 处理事件:当有事件发生时,Redis 会调用相应的事件处理器来处理这些事件(如读入客户端请求、发送响应等)。
  5. 循环处理:重复上述步骤,直到 Redis 关闭。

高效数据结构

Redis采用高效的数据结构来提升性能,比如动态字符串、压缩列表、跳跃表等。

动态字符串:

动态字符串(SDS,Simple Dynamic String)是Redis存储字符串的底层实现,它可以动态调整大小,高效的进行字符串的追加、删除等操作

SDS本身包含了长度信息,能够以O(1)的复杂度获取字符串的长度信息。另外,SDS的空间分配策略和惰性空间释放策略,让字符串的操作更加高效。当然,SDS需要额外的空间存储这样的信息,导致它占用的内存较大。

struct sdshdr {
    long len;
    long free;
    char buf[];
};

buf是保存实际数据的字节数组,而len为buf的实际长度,free表示额外的可用字节大小。压缩列表:压缩列表(ziplist)是一种紧凑型的数据结构,它比较适合用来存储一些较小的元素,比如字符串、整数或者浮点数等。压缩列表由一系列的条目组成,每个条目表示一个元素,它包括以下几个部分:

Prevlen:前一个条目的长度

Entrylen:当前条目的长度
Content:元素的实际内容列表中的这些元素都是连续的,这意味着不需要额外的元数据或者指针来记录元素间的关系。与其它的数据结构相比,比如双向链表,压缩列表减少了额外的内存开支。同时,这些元素是连接存放的,所以按序号访问时,用时都是常数时间。另外,Redis会自动在压缩列表和其他数据结构之间进行切换,比如双向链表,哈希表等。切换的标准与元素的大小和个数有关。跳表:

跳表(skiplist)是一种有序的数据结构,能够进行快速查找。通过层级的链表结构,实现了元素的快速操作,包括插入、删除等。

跳表在多个层级上建立索引,可以在O(logN)的时间内完成查找。通过跳跃这些元素的上层指针,可以跳过很多元素,从而实现快速的查找、插入和删除操作。

Redis还支持三大扩展数据类型:位图Bitmaps、基数统计HyperLogLog、地理位置GEO、Streams等。

单线程

Redis是一款内存数据库,绝大部分的操作都是在内存中进行的,所以它的性能瓶颈主要是内存操作和网络通信,而不是CPU。在这样的场景下,多线程就未必比单线程要快,因为CPU在一个时间片里只能执行一个线程,线程切换的时候就需要保存当前线程的上下文,包括一些额外的线程调度、同步等操作,这样就会降低Redis性能。

Redis不是CPU密集型的系统,单个线程足以满足计算要求,这样就避免了多线程带来的性能损耗,同时也让操作更加稳定。

Redis 使用单线程来处理客户端的请求,这是因为:

  1. 避免锁竞争:多线程编程中常见的锁竞争问题会导致性能下降。使用单线程可以避免这一问题,从而提供更高的性能和一致性。
  2. 简化编程:单线程模型使得代码更简单,没有复杂的线程同步问题,更容易维护和调试。

Redis 的单线程模型是指其主要的网络 I/O 和命令执行部分是由一个单线程来处理的。但这并不意味着 Redis 完全依赖单线程来处理所有的操作。实际上,Redis 利用了多种优化技术和机制来确保高效的性能。

Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

多线程的使用

虽然 Redis 的核心操作是单线程的,但它在某些场景下也利用了多线程来优化性能。例如:

  1. 异步 I/O 操作
    • Redis 使用非阻塞 I/O 多路复用技术(如 epoll、kqueue 等)来处理网络连接和 I/O 操作。这使得即使单线程也能够高效地处理大量并发连接。
  2. 持久化操作
    • Redis 提供了 RDB(快照)和 AOF(追加文件)两种持久化机制。在执行这些持久化操作时,Redis 会通过后台子进程或线程来处理,以避免阻塞主线程。
    • 例如,RDB 快照的生成是在一个子进程中进行的,而 AOF 重写则可以在后台线程中进行。
  3. 模块扩展
    • Redis 4.0 引入了模块系统,允许开发者编写自定义的 Redis 扩展。模块可以使用多线程来实现一些复杂的计算或 I/O 操作,而不会阻塞 Redis 的主线程。