Redis中的阻塞点
Redis作为一个高性能的缓存中间件,在进行网络IO以及键值对读写时仅仅使用的单线程,如果产生阻塞将无法正常响应客户端,所以我们需要了解Redis中到底存在哪些阻塞操作,我们可以按照Redis的关联对象分为如下四种大类。
-
客户端:键值对的增删改查、网络IO、数据库级别的操作(FLUSHALL、FLUSHDB)。
-
磁盘:持久化操作RDB快照、AOF追加日志、AOF日志重写。
-
主从节点:主从同步。
-
切片集群:向其它实例同步哈希槽信息、数据迁移(负载均衡有热点数据访问量过大,需要重新分配哈希槽)。
客户端阻塞操作
键值对增删改查
键值对的增删改查是和客户端的主要交互部分,如果操作的复杂度过高是会阻塞主线程的,例如查询全部key值keys *
、查询Set的所有value值SMEMBERS
、查询Hash的全量查询HGETALL
以及各种聚合操作(交集、并集、差集等)这些都有可能阻塞主线程,这也就是阻塞点之一。
除了复杂的查询操作外,我们还需要知道删除操作也是存在阻塞风险,虽然删除的时间复杂度为O(1),因为Redis的键值数据保存在全局哈希表中,删除的效率自然高但删除的步骤不仅仅只是删除元素而已,具体分为如下步骤
-
将对应键值对占用的内存空间释放。
-
将释放的内存空间插入到空闲内存块链表中,方便后续内存的管理和分配。
所以一旦删除多个元素,那么插入空闲内存块链表的时间就会增长,这里需要特别注意的就是bigkey的删除,虽然删除的元素不多,但是删除后释放的空间大,这样插入到空闲内存块链表中的内存也会增大,从而导致阻塞。
网络IO
Redis在网络层面采用了多路复用IO模型,采用网络套接字避免了建立连接和等待请求到达这些阻塞操作,所以网络IO并不是阻塞点。
数据库级别的操作
数据库级别的操作其实针对的就是FLUSHALL、FLUSHDB这类操作,删除key既然有阻塞的风险那么清空数据库肯定会释放大量的内存空间,也会有阻塞的风险。
磁盘操作
持久化操作RDB文件,主线程会fork子进程完成,需要注意的是fork一瞬间对主线程是阻塞的,而且实例越大对主线程的阻塞时间就越长因为fork的时候会复制主内存的虚拟页表。
重写AOF文件,主线程会fork子进程bgrewriteaof完成。
至于追加写入AOF文件这个根据回写策略(Always、Everysec、No)除了No会由操作系统自己决定写入,其余两种都需要主线程同步写入,如果大量的写操作需要记录到AOF文件中自然就会导致主线程阻塞。
主从节点操作
主从节点间的操作一般是指主从节点同步,主从节点同步一般包括主线程生成RDB文件、传输给从库、从库清空数据库、加载RDB文件。
主从同步中的RDB文件生成主线程和fork子进程完成,而清空数据库已经分析过,那么将RDB文件加载进入内存其实也是一个阻塞操作,文件越大阻塞时间越长。
切片集群操作
对于我们的切片集群Cluster,每个实例主节点都会保存一部分哈希槽的信息,这个信息会在所有主实例间传递这个过程是不会阻塞主线程的,而对于数据迁移Redis Cluster采用同步迁移方案如果存在bigkey那么将阻塞主线程如果不存在bigkey那么将不会阻塞主线程。
阻塞点分析
综上分析得出以下五个阻塞点。
-
键值对的全量查询或者聚合查询
-
bigkey的删除
-
数据库级别操作也就是FLUSHALL、FLUSHDB
-
AOF追加写入
-
主从同步从库RDB文件加载
在Redis主线程上直接执行上述操作显然会导致阻塞,为了避免这种情况Redis提供了异步线程机制,但并不是所有的阻塞点都能异步执行,需要分析是否为关键路径操作。
非关键路径客户端不需要使用执行操作的结果,直接返回OK给客户端就行如客户端1所示,而关键路径是客户端需要等待线程的执行结果,所以无论是主线程执行还是子线程执行都会阻塞,如客户端2所示。
-
键值对的全量查询或者聚合查询其实都是需要客户端等待Redis响应结果,所以查询是关键路径操作,不能采用异步执行。
-
bigkey的删除这是一个典型的非关键路径不需要向客户端返回具体的值为非关键路径,可以异步执行。
-
数据库操作FLUSHALL、FLUSHDB和bigkey的删除类似也是非关键路径。
-
AOF追加写入一般认为是同步写入,但写入后不需要返回客户端详细结果,所以也可以异步执行。
-
RDB文件加载必须要阻塞执行,因为客户端需要从节点提供读取服务需要保证数据的完整性,不可异步执行。
综上bigkey的删除、数据库清空操作、AOF日志追加操作可以异步执行。
异步子线程机制
Redis拥有自己的一套异步机制,当Redis启动后会调用系统函数pthread_create创建子线程,如图中提到复杂AOF日志追加、键值删除或清空数据库、文件关闭的异步执行。
当客户端发起操作时,主线程会将数据库操作封装为一个任务以删除为例放入到任务队列中,同时向客户端发送一个完成的信息表明删除完成,这时删除操作其实还未进行,等到后续子线程从任务队列中读取任务时才是真正的删除开始,这也被称为惰性删除。
需要注意的是异步删除键值对方案和异步数据库清空方案都是4.0以后的版本提出
-
键值对删除少量数据还是可以适用del做同步删除,而bigkey的删除推荐使用UNLINK,该命令会在另外的一个线程回收内存异步进行。
-
数据库清空可以采用4.0提出的异步方案FLUSHALL、FLUSHDB,加入异步指令
FLUSHALL ASYNC
,FLUSHDB ASYNC
即可异步执行。
而前面提到的AOF追加写入日志在回写策略中配置了everysec(配置No不由Redis写入是操作系统负责写入,配置always只能主线程完成)选项后,主线程将AOF写日志封装成一个任务放入任务队列中,后续子线程读取任务后开始异步写入。