共计 3321 个字符,预计需要花费 9 分钟才能阅读完成。
Redis 的高性能怎么做到的?
Redis
这个 NOSQL
数据库在计算机界堪称是无人不知,无人不晓。只有波及到数据那么就须要数据库,数据库类型很多,然而 NOSQL
的kv
内存数据库也很多,redis
作为其中一个是怎么做到行业天花板的呢?是怎么做到高性能的呢?怎么做到高可用的呢?明天这篇八股文我就整顿一些 redis
的设计写写,本篇还是偏对于 高性能
这一块。
高效数据结构
Redis
的数据库相比传统的关系数据库,在数据结构上也是比拟非凡的,它的所有数据类型都能够看做是一个 map
的构造,key
作为查问条件。
Redis
基于 KV
内存数据库,它外部构建了一个哈希表,依据指定的 KEY
拜访时,只须要 O(1)
的工夫复杂度就能够找到对应的数据,而 value
的值又是一些领有各种个性的数据结构,这就给 redis
在数据操作的时候提供很好的性能了。
基于内存存储
相比传统的关系数据库,数据文件可能以 lsm tree 或者 b+ tree
模式存在硬盘上,这个时候读取文件要有 io
操作了,而 redis
在内存中进行,并不会大量耗费 CPU
资源,所以速度极快。
内存从上图能够看到它介于硬盘和 cpu
缓存两头的,相比硬盘查找数据必定是快的,当然这里笔者个人见解上,如果关系型数据库把一些平庸操作的数据库也搁置在内存中缓存,也会失去一些性能的晋升,像操作系统外面缺页异样一样解决,把数据片段通过一些非凡算法缓存在内存外面,缩小文件 io
的开销。
io 多路复用
传统对于并发状况,如果一个过程不行,那搞多个过程不就能够同时解决多个客户端连贯了么?多过程是能够解决一些并发问题,然而还是有一些问题,上下文切换开销,线程循环创立,从 PCB
来回复原效率较低。随着客户端申请增多,那么线程也随着申请数量直线回升,如果是并发的时候波及到数据共享拜访,有时候波及到应用锁来管制范畴程序,影响其余线程执行效率。(过程在 Linux
也能够了解为线程,每个过程只是有一个线程,当然这里我下面写的过程,别纠结这些。。。)
线程
是运行在过程上下文的逻辑流,一个过程能够蕴含多个线程,多个线程运行在同一过程上下文中,因而可共享这个过程地址空间的所有内容,解决了过程与过程之间通信难的问题,同时,因为一个线程的上下文要比一个过程的上下文小得多,所以线程的上下文切换,要比过程的上下文切换效率高得多。
像 redis
和Nginx
这种利用就是单线程的程序,为什么他们能做到这么强的性能?首先看一个例子:
- Blocking IO
中午吃饭,我给餐厅老板说要一碗‘热干面’,而后我就在那边始终等着老板做,老板没有做好,我就始终在哪里等着什么也不做,直到‘热干面’做好。
这个流程就是咱们常说的 Blocking I/O
如图:
同步 阻塞 IO
模型中,应用程序发动 read
调用后,会始终阻塞,直到内核把数据拷贝到用户空间。
- Non Blocking IO
切换一下常见:
同样你中午吃饭,给餐厅老板说要一碗‘热干面’,而后老板开始做了,你每隔几分钟向老板问一下‘好了吗?’,直到老板说好了,你取到‘热干面’完结。
同步 非阻塞 IO
模型中,应用程序会始终发动 read
调用,期待数据从内核空间拷贝到用户空间的这段时间里,线程仍然是阻塞的,直到在内核把数据拷贝到用户空间,通过轮询操作,防止了始终阻塞,取回 热干面
的过程就是内核把筹备好的数据交换到用户空间过程。
综上两种模型,毛病都是差不多,都是在期待内核筹备数据,而后阻塞期待,同样逃不开阻塞这个问题,应用程序一直进行 I/O
零碎调用轮询数据是否曾经筹备好的过程是非常耗费 CPU
资源的。
- I/O Multiplexing
还是之前那个例子:
中午吃饭,给餐厅老板说要一碗‘热干面’,而后老板安顿给上面的厨子做,具体哪个厨子做不晓得,有好几个厨子,而后老板每隔一段时间询问上面的厨子有木有做好,如果做好了,就告诉我来去取餐。
IO
多路复用模型中,线程首先发动 select
调用,询问内核数据是否准备就绪,等内核把数据筹备好了,用户线程再发动 read
调用,read
调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
Reactor
通过 I/ O 复用
程序监控客户端申请事件,收到事件后通过工作分派器进行散发。针对建设连贯申请事件,通过 Acceptor
解决,并建设对应的 handler
负责后续业务解决,针对非连贯事件,Reactor
会调用对应的 handler
实现 read->
业务解决->write
解决流程,并将后果返回给客户端,整个过程都在一个线程里实现。
Redis
是基于 Reactor
单线程模式来实现的,IO 多路复
用程序接管到用户的申请后,全副推送到一个队列里,交给文件分派器。对于后续的操作,和在 reactor
单线程实现计划里看到的一样,整个过程都在一个线程里实现,因而 Redis
被称为是单线程的操作。
咱们平时说的 Redis
单线程快是指它的申请处理过程十分地快!在单线程中监听多个 Socket
的申请,在任意一个 Socket
可读 / 可写时,Redis
去读取客户端申请,在内存中操作对应的数据,而后再写回到 Socket
中。
单线程的益处:
- 没有了访问共享资源加锁的性能损耗
- 开发和调试十分敌对,可维护性高
- 没有多个线程上下文切换带来的额定开销,不是没有,是缩小了
单线程不是没有毛病,其实毛病也是很显著的,如果前一个申请产生耗时比拟久的操作,那么整个 Redis
就会阻塞住,其余申请也无奈进来,直到这个耗时久的操作解决实现并返回,其余申请能力被解决到,然而 redis
应用的Reactor
单线程模式来实现的能够缓解这种状况。
在
Redis 4.0
之后的版本,引入多线程,而这个多线程是只的异步开释内存,它次要是为了解决在开释大内存数据导致整个redis
阻塞的性能问题,单机redis
如果解决大数据申请时还是会呈现瓶颈,然而redis
有集群高可用解决方案能够解决,主节点只负责写,从节点负责读,io
复用先写到这里,集群高可用我会另外在出一篇文章。
写时拷贝
有了高效的数据结构和 io
多路模型,目前能解决数据拜访效率问题,然而 redis
为了保障了数据不失落有快照机制,说到快照那么会操作磁盘,redis
怎么解决的在数据操作的时候并且还能保证数据记录完整性的?不影响数据拜访效率的呢?
答案是用了 写时复制技术
,什么是 写时复制
?如果你是一个科班的或者你的操作系统学的不错的话,这个问题很分明。
在操作系统设计中过程的内存可分为 虚拟内存
和 物理内存
,什么是虚拟内存?你能够去看我上一篇文章 Virtual Memory。redis
会从主过程中通过 fork()
零碎调用,创立一个子过程,将父过程的 虚拟内存
与 物理内存
映射关系复制到子过程中,并将设置内存共享的,子过程只负责将内存外面数据写入到rdb
进行长久化操作,如果在操作的时候主过程对内存批改了,应用 写时拷贝
技术,将对应的内存创立一个正本而后进行写入长久化。
如上图主过程则提供服务,只有当有人批改以后内存数据时,才去复制被批改的内存页,用于生成快照。
管道通信
除了本地服务器内存和数据结构的操作影响客户端读写效率的还有网络起因。redis
的通信协定是用一种文件协定,有趣味本人去钻研钻研吧,我这里不打算写。每次客户端操作的时候,命令和元数据都被打包成 redis
协定进行传输到服务器上。
依照这样那每个命令的执行工夫:客户端发送工夫 + 服务器解决和返回工夫 + 一个网络来回的工夫。
从上图能够看进去如果每操作一条命令,那么就要执行一次 网络 io
,如果客户端频繁操作数据那么就频繁网络操作,这个过程也是十分耗时的,影响性能的。redis
在客户端程序中做了一些优化引入了一个管道(pipelining
)概念。
管道会把多条无关命令批量执行,以缩小多个命令别离执行带来的网络交互工夫,在一些批量操作数据的场景。
小结
简略始于简单!
,别看客户端就几个简略 api call
的事件,这前面还有很多设计值得去学习,看完这篇八股文你或者对 redis
高性能有新的意识了,不要小看某些细节优化和解决方案选型,有时候能够带来显著性能晋升。当然这篇文章没有把 redis
设计写完,例如还有 aof
的内核文件描述符映射,异步写数据到硬盘上,零拷贝技术等等。。。。后续文章将会更新redis 高可用是怎么做到的?