Redis核心技术与实战笔记2-Redis为什么快?

目录

在后端面试的时候基本都会问的一个问题:Redis为什么快?不知道你会怎么回答,这篇文章主要整理的就是关于这个问题。

Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。而Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

Redis 为什么用单线程?

“使用多线程,可以增加系统吞吐率,或是可 以增加系统扩展性。” 这是我们经常听到的。 但是,请你注意,通常情况下,在我们采用多线程后,如果没有良好的系统设计,实际得 到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但 是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。

BDaJyj.jpg

为什么会出现这种情况?系统中通常会存在被多线程同时访问的 共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共 享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开 销。

Redis 有 List 的数据类型,并提供出队(LPOP) 和入队(LPUSH)操作。假设 Redis 采用多线程设计,如下图所示,现在有两个线程 A 和 B,线程 A 对一个 List 做 LPUSH 操作,并对队列长度加 1。同时,线程 B 对该 List 执行 LPOP 操作,并对队列长度减 1。为了保证队列长度的正确性,Redis 需要让线程 A 和 B 的 LPUSH 和 LPOP 串行执行,这样一来,Redis 可以无误地记录它们对 List 长度的修 改。否则,我们可能就会得到错误的长度结果。这就是多线程编程模式面临的共享资源的 并发访问控制问题。

BDdQBR.jpg

并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是 简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也 在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增 加。

而且,采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统 代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

Redis 为什么那么快?

关于这个问题总结如下:

  • Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,如哈希 表和跳表
  • Redis采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率

Redis 中IO的阻塞点

假如我们从Redis中获取GET 获取一个Key的value,redis为了处理这个请求,,需要监听客户端请求 (bind/listen),和客户端建立连接(accept),从 socket 中读取请求(recv),解析 客户端发送请求(parse),根据请求类型读取键值数据(get),最后给客户端返回结 果,即向 socket 中写回数据(send)。

BDbE9K.jpg

上图中潜在的网络IO阻塞点:分别是 accept()recv()。当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这 里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv() 从一个客户端 读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。不过socket 网络模型本身支持非阻塞模式。

非阻塞模式

Socket 网络模型的非阻塞模式设置,主要体现在如下三个关键的函数调用上:

BDOKHJ.jpg

基于多路复用的高性能 I/O 模型

Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。在 Redis 只运行单线程的情况下,该机制允许内核中,同 时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据 请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

BDO2Dg.jpg

Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理 上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。 为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针 对不同事件的发生,调用相应的处理函数。

这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来, Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时, Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件 的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

总结

  • Redis 单线程是指它对网络 IO 和数据读写的操作采用了一个线程
  • 采用单线程的一个核心原因是避免多线程开发的并发控制问题
  • 单线程的 Redis 也能获得高性能,跟多路复用的 IO 模型密切相关,因为这避免了 accept() 和 send()/recv() 潜在的 网络 IO 操作阻塞点。