本文主要是笔者学习redis的笔记, 为了更好的学习和理解, 主要分为高可用、高可靠以及高扩展三大板块来学习, 如下图:
高可用、高性能以及高扩展对于任何一个大型系统的设计来说都是必不可少的, 笔者希望自己不仅仅是学习redis基本原理, 更要通过对优秀的中间件思考, 能逐步沉淀自己设计大型系统架构的能力. 废话不多说了, 直接上菜,接下来为大家分享redis高性能.
任何系统设计都会有一些取舍, redis也不例外, 本文主要从redis的高性能中一些取舍做了一些小结, 采用的形式为QA方式, 具体如下:
高性能 Q: redis的高性能体现在哪些方面?A:
- 基于内存读写. Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
- 采用单线程, 避免线程线程上线文切换引起资源消耗, 以及相比多线程而言, 单线程不需要对共享资源进行加锁处理, 避免多线程对共享资源竞争导致性能消耗;
- 采用非阻塞IO多路复用机制, 提交网络吞吐量;
A: 首先, 需要厘清一个事实, redis单线程, 主要是指redis的网络IO和键值对读写是由单个线程完成的, 这也是redis对外提供键值存储服务的主要流程. 但是redis的其他功能, 比如持久化、异步删除、集群数据同步等是由额外的线程执行的. 其次: redis真正影响性能的并不是get/set操作, 而是网络IO, 因此redis采用了IO多路复用机制.
单线程优缺点优点:
- redis 基于内存操作, 并且采用了hash表和跳表等数据结构, 操作速度非常快;
- 避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- redis采用IO非阻塞的IO, 增加网络的吞吐量;
缺点:
主要包括两方面
- 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
- 操作bigKey: 写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
- 大量key集中过期 : Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
- 淘汰策略: 淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key(比如采用最久未使用淘汰策略, 需要进行淘汰策略),也会造成耗时变长;
- 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
- AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
- 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
A: 为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。
哈希表的 O(1) 复杂度和快速查找特性
A: Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
A: Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
Q: 如何进行rehash 操作?A: 为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
- 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
- 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
- 释放哈希表 1 的空间。
到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。
Q:大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞, 无法服务其他请求,如何处理?A: 为了避免这个问题,Redis 采用了渐进式 rehash。
Q: redis 如何实现渐进式 rehash?A: 简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:
A: 渐进式rehash执行时,除了根据键值对的操作来进行数据迁移,Redis本身还会有一个定时任务在执行rehash,如果没有键值对操作时,这个定时任务会周期性地(例如每100ms一次)搬移一些数据到新的哈希表中,这样可以缩短整个rehash的过程。
Q: 什么是压缩表?A:压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
A: 增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位
各种数据结构时间复杂度:
A:
基本 IO 模型与阻塞点以 Get 请求为例,SimpleKV 为了处理一个 Get 请求,需要监听客户端请求(bind/listen),和客户端建立连接(accept),从 socket 中读取请求(recv),解析客户端发送请求(parse),根据请求类型读取键值数据(get),最后给客户端返回结果,即向 socket 中写回数据(send)。
其中,bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get 属于键值数据操作。既然 Redis 是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。 如下图:
但是,在这里的网络 IO 操作中,有潜在的阻塞点,分别是建立链接accept() 和 读取请求recv()。当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。
基于多路复用的高性能 I/O 模型
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。
简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
下图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。
为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
A: **epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。**这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。
例如: 以连接请求和读数据请求为例。这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。
Q: select poll epoll的区别?A: select和poll本质上没啥区别,就是文件描述符数量的限制,select根据不同的系统,文件描述符限制为1024或者2048,poll没有数量限制。他两都是把文件描述符集合保存在用户态,每次把集合传入内核态,内核态返回ready的文件描述符。
epoll是通过epoll_create和epoll_ctl和epoll_await三个系统调用完成的,每当接入一个文件描述符,通过ctl添加到内核维护的红黑树中,通过事件机制,当数据ready后,从红黑树移动到链表,通过await获取链表中准备好数据的fd,程序去处理。 详情见 epoll详解
小结本文主要从redis高性能做的小结, 后续还会有redis高可用、高扩展小结;



