The website uses cookies. By using this site, you agree to our use of cookies as described in the Privacy Policy.
I Agree
blank_error__heading
blank_error__body
Text direction?

面试官:Redis 为什么这么快?除了基于内存操作还有其他原因吗?

前言

曾经有人这么告诉我:“在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。另外,超过一弹指的耗时操作要有进度提示,并且可以随时中止或取消,这样才能给用户最好的体验。”

那么瞬间、刹那、一弹指具体是多少时间呢?

根据《摩诃僧祗律》记载:

一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。

那么,经过周密的计算,一瞬间为0.36 秒,一刹那有 0.018 秒.一弹指长达 7.2 秒。

为了提升用户体验,提高网站响应速度,一般都会使用缓存,而通常的技术选型都是redis。

今天我们就来聊聊,为什么redis这么快!

面试问题

“Redis 作为缓存大家都在用,那 Redis 一定很快咯?”,我问一个前来面试的同学。

“是的,因为Redis 完全是基于内存的操作,所以很快。”,面试的同学回答道。

“还有其他的原因吗?”,我一脸坏笑的追问道。对面的同学有点不知如何回答。

计算机的世界,缓存无处不在

浏览器有缓存,CPU有缓存,磁盘有缓存,CDN缓存,app应用有缓存,数据库有缓存。。。

为什么到处都是缓存?答案一个字:快!!!

浏览器、App有缓存,是为了能让用户更快的访问以前访问过的页面,提升用户体验;CPU、磁盘有缓存,是为了更快的处理数据,提高计算机的性能;数据库有缓存,是为了让业务服务器更快的处理业务。

作为java程序猿,一说到缓存,就想到了Redis。

官方文档介绍说,Redis的操作都是基于内存的,CPU不是 Redis性能瓶颈,,Redis的瓶颈是机器内存和*网络带宽*。

Reds是C语言写的,性能极高。单台redis情况下,官方提供的数据为:读的速度是110000次/s,写的速度是81000次/s

但是redis的线程模型表明,redis是单进程、单线程的。是不是很amazing!!!

Redis为什么这么快?

先说两个误区:一,高性能的服务器不一定都是多进程、多线程的;二、多线程不一定都不比单线程快,比如单核机器。

在我们通常的认知中,高性能都是通过多进程、多线程实现的。比如Nginx是多进程单线程的,Memcached是单进程多线程的。

在计算机的世界中,CPU的速度是远大于内存的速度的,同时内存的速度也是远大于硬盘的速度。redis的操作都是基于内存的,绝大部分请求是纯粹的内存操作,非常迅速,使用单线程可以省去多线程时CPU上下文会切换的时间,也不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。对于内存系统来说,多次读写都是在一个CPU上,没有上下文切换效率就是最高的!既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章的采用单线程的方案了(毕竟采用多线程会有很多麻烦)。

那么Redis的单进程单线程模型的具体细节是怎样的?

IO多路复用技术

首先说一下,什么是IO多路复用技术。

比如,现在我们模拟一个tcp服务器处理30个客户的socket,如何快速的处理掉这30个请求呢?

在不了解原理的情况下,我们类比一个实例:在课堂上让全班30个人同时做作业,做完后老师检查,30个学生的作业都检查完成才能下课。如何在有限的资源下,以最快的速度下课呢?

  • 第一种:安排一个老师,按顺序逐个检查。先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。这种方式只需要一个老师,但是耗时时间会比较长。
  • 第二种:安排30个老师,每个老师检查一个学生的作业。 这种类似于为每一个socket创建一个进程或者线程处理连接。这种方式需要30个老师(最消耗资源),但是速度最快。
  • 第三种:安排一个老师,站在讲台上,谁解答完谁举手。这时C、D举手,表示他们作业做完了,老师下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。这种方式可以在最小的资源消耗的情况下,最快的处理完任务。

第三种就是IO复用模型(Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。)

Redis线程模型

Redis基于Reactor模式开发了自己的网络事件处理器,被称为文件事件处理器,由套接字、I/O多路复用程序、文件事件分派器(dispatcher),事件处理器四部分组成。

I/O多路复用程序、文件事件分派器

I/O多路复用程序会同时监听多个套接字,当被监听的套接字准备好执行accept、read、write、close等操作时,与操作相对应的文件事件就会产生,I/O多路复用程序会将所有产生事件的套接字都压入一个队列,然后以有序地每次仅一个套接字的方式传送给文件事件分派器,文件事件分派器接收到套接字后会根据套接字产生的事件类型调用对应的事件处理器。

事件的处理器

(1)连接应答处理器:

当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AEREADABLE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候,套接字就会产生AEREADABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作。

(2)命令请求处理器:

当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AEREADABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AEREADABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作;

在客户端连接服务器的整个过程中,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。

(3)命令回复处理器:

当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AEWRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AEWRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作。

当命令发送完毕后,服务器会解除命令回复处理器与客户端套接字的AE_WRITABLE事件之间的关联。

  • 注意1:只有当上一个套接字产生的事件被所关联的事件处理器执行完毕,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字,所以对每个命令的执行时间是有要求的,如果某个命令执行过长,会造成其他命令的阻塞。所以慎用O(n)命令,Redis是面向快速执行场景的数据库。
  • 注意2:命令的并发性。Redis是单线程处理命令,命令会被逐个被执行,假如有3个客户端命令同时执行,执行顺序是不确定的,但能确定不会有两条命令被同时执行,所以两条incr命令无论怎么执行最终结果都是2。

客户端与redis通信过程

1、假设一个Redis服务器正在运作,那么这个服务器的监听套接字的 AE_READABLE 事件应该正处于监听状态之下, 而该事件所对应的处理器为连接应答处理器。

2、如果这时有一个Redis客户端向服务器发起连接,那么监听套接字将产生 AEREADABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AEREADABLE事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。

3、之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生 AE_READABLE 事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。

4、执行命令将产生相应的命令回复, 为了将这些命令回复传送回客户端, 服务器会将客户端套接字的 AEWRITABLE 事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候, 客户端套接字将产生 AEWRITABLE 事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。

压力测试

redis安装完成后,有一个命令叫redis-benchmark。

这个命令是官方自带的压力测试工具。下面我们就是用这个命令的默认参数简单的做一下压测。

可以看出

  • 一共发了10万个set请求,一共耗时0.79秒
  • 一共发了10万个get请求,一共耗时0.81秒
$ ./redis-benchmark
====== PING_INLINE ======
  100000 requests completed in 0.85 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.98% <= 1 milliseconds
100.00% <= 1 milliseconds
117924.53 requests per second

====== PING_BULK ======
  100000 requests completed in 0.81 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

100.00% <= 0 milliseconds
123915.74 requests per second

====== SET ======
  100000 requests completed in 0.79 seconds   #一共发了10万个set请求,一共耗时0.79秒
  50 parallel clients  #一共使用50个客户端并发请求
  3 bytes payload  #每个请求set3个字节
  keep alive: 1   #使用一个redis服务器,单机测试

99.90% <= 1 milliseconds  #99.9%的请求的耗时是小于等于1毫秒的
99.95% <= 2 milliseconds
99.96% <= 3 milliseconds
100.00% <= 3 milliseconds
127388.53 requests per second #每秒处理127388.53个请求

====== GET ======
  100000 requests completed in 0.81 seconds#一共发了10万个get请求,一共耗时0.81秒
  50 parallel clients        #一共使用50个客户端并发请求
  3 bytes payload   #每个请求get3个字节
  keep alive: 1    #使用一个redis服务器,单机测试

99.99% <= 1 milliseconds #99.99%的请求的耗时是小于等于1毫秒的
100.00% <= 1 milliseconds
123456.79 requests per second    #每秒处理123456.79个请求

也可以设置并发连接数-c、请求数-n、字节数-d参数等。

总结

  • redis是纯内存操作:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础。
  • 非阻塞I/O:Redis采用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了时间,不在I/O上浪费过多的时间。
  • 单线程避免了线程切换和竞态产生的消耗。

Redis采用单线程模型,每条命令执行如果占用大量时间,会造成其他线程阻塞,对于Redis这种高性能服务是致命的,所以Redis是面向高速执行的数据库

完成,收工!

Measure
Measure
Related Notes
Get a free MyMarkup account to save this article and view it later on any device.
Create account

End User License Agreement

Summary | 5 Annotations
官方提供的数据为:读的速度是110000次/s,写的速度是81000次/s 。
2020/09/20 10:01
单进程、单线程的
2020/09/20 10:01
select、poll和epoll
2020/09/20 10:03
事件驱动
2020/09/20 10:04
网络事件处理器
2020/09/20 10:05