跳转至

Redis速度快的原因

吞吐量测试

Redis 官方提供了一个测试脚本,可以供我们测试 Redis 的吞吐量:

redis-benchmark -q -n 100000:测试常用命令的吞吐量。

redis-benchmark -t set,lpush -n 100000 -q:测试 Redis 处理 set 和 lpush 命令的吞吐量。

redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')":测试 Redis 处理 Lua 脚本等吞吐量。

单线程与多线程

Redis 从 4.0 版本开始就有了多线程的概念,虽然处理命令请求的核心模块确实是保证了单线程执行,然而在其它许多地方已经有了多线程,比如:在后台删除对象,通过 Redis 模块实现阻塞命令,生成 dump 文件,以及 6.0 版本中网络 I/O 实现了多线程等,而且在未来 Redis 应该会有越来越多的模块实现多线程。

所谓的单线程,只是说 Redis 处理客户端的请求(即执行命令)时,是单线程去执行的,并不是说整个 Redis 都是单线程。

选择使用单线程来执行请求

Redis 为什么会选择使用单线程呢?这是因为 CPU 成为 Redis 瓶颈的情况并不常见,成为 Redis 瓶颈的通常是内存或网络带宽。例如,在一个普通的 Linux 系统上使用 pipelining 命令,Redis 可以每秒完成 100 万个请求,所以如果我们的应用程序主要使用 O(N)O(log(N)) 复杂度的命令,它几乎不会使用太多的 CPU。

那么既然 CPU 不会成为瓶颈,理所当然的就没必要去使用多线程来执行命令,这里需要明确的一个问题就是:多线程一定会比单线程快吗?答案是不一定。因为多线程也是有代价的,最直接的两个代价就是线程的创建和销毁(当然可以通过线程池来一定程度减少频繁的创建线程和销毁线程)以及线程的上下文切换。

在我们的日常系统中,主要可以区分为两种:CPU 密集型 和 IO 密集型:

  • CPU 密集型:这种系统就说明 CPU 的利用率很高,那么使用多线程反而会增加上下文切换而带来额外的开销,所以使用多线程效率可能会不升反降。

举个例子:假如你现在在干活,你一直不停的在做一件事,需要 1 分钟可以做完,但是你中途总是被人打断,需要花 1 秒钟时间步行到旁边去做另一件事,假如这件事也需要 1 分钟,那么你因为反复切换做两件事,每切换一次就要花 1 秒钟,最后做完这 2 件事的时间肯定大于 2 分钟(取决于中途切换的次数),但是如果中途不被打断,你做完一件事再去做另一件事,那么你最多只需要切换 1 次,也就是 2 分 1 秒就能做完。

  • IO 密集型:IO 操作也可以分为磁盘 IO 和网络 IO 等操作。大部分 IO 操作的特点是比较耗时且 CPU 利用率不高,所以 Redis 6.0 版本网络 IO 会改进为多线程。至于磁盘 IO,因为 Redis 中的数据都存储在内存(也可以持久化),所以并不会过多的涉及到磁盘操作。

举个例子:假如你现在给树苗浇水,你每浇完一次水之后就需要等别人给你加水之后你才能继续浇,那么假如这个等待过程需要 5 秒钟,也就是说你浇完一次水就可以休息 5 秒钟,而你切换去做另一件事来回只需要 2 秒,那么你完全可以先去做另一件事,做完之后再回来继续浇水,这样就可以充分利用你空闲的 5 秒钟时间,从而提升了效率。

使用多线程还会带来一个问题就是数据的安全性,所以多线程编程都会涉及到锁竞争,由此也会带来额外的开销。

IO 多路复用机制

I/O 指的是网络 I/O, 多路指的是多个 TCP 连接(如 Socket),复用指的是复用一个或多个线程。I/O 多路复用的核心原理就是不再由应用程序自己来监听连接,而是由服务器内核替应用程序监听。

在 Redis 中,其多路复用有多种实现,如:select,epoll,evport,kqueue 等。

我们用去餐厅吃饭的例子来解释一下 I/O 多路复用机制(点餐人相当于客户端,餐厅的厨房相当于服务器,厨师就是线程)。

  • 阻塞 IO:张三去餐厅吃饭,点了一道菜,这时候他啥事也不干了,就是一直等,等到厨师炒好菜,他就把菜端走开始吃饭了。也就是在菜被炒好之前,张三被阻塞了,这就是 BIO(阻塞 IO),效率非常低下。
  • 非阻塞 IO:张三去餐厅吃饭,点了一道菜,这时候张三他不会一直等,找了个位置坐下,刷刷抖音,打打电话,做点其它事,然后每隔一段时间就去厨房问一下自己的菜好了没有。这种就属于非阻塞 IO,这种方式虽然可以提高性能,但是如果有大量 IO 都来定期轮询,也会给服务器造成非常大的负担。
  • 事件驱动机制:张三去餐厅吃饭,点了一道菜,这时候他找了个位置坐下来等,接下来厨房(服务器)有两种做法:
  • 厨房把菜做好了直接把菜端出去,但是端菜的人并不知道这道菜是谁的,于是就挨个询问顾客,这就是多路复用中的 select 模型,不过 select 模型最多只能监听 1024 个 socket(poll 模型解决了这个限制问题)。
  • 厨房把菜做好了直接把菜放在窗口上,大喊一声:“某某菜做好了,是谁的快过来拿。”这时候听到通知的顾客就会自己去拿,这就是多路复用中的 epoll 模型。

需要注意的是:在 IO 多路复用机制下,客户端可以阻塞也可以选择不阻塞(大部分场景下是阻塞 IO),这个要具体情况具体分析,但是在多路复用机制下,服务端就可以通过多线程(上面示例中可以多几个厨师同时炒菜)来提升并发效率。