Redis实例在运行的时候,要和许多对象进行交互,这些不同的交互对象会有不同的操作。下面我们来看看,这些不同的交互对象以及相应的主要操作有哪些。
-
客户端:键值对的增删改查操作。
-
磁盘:生成RDB快照、记录AOF日志、AOF日志重写。
-
主从节点:主库生成、传输RDB文件,从库接受RDB文件、清空数据库、加载RDB文件。
下面我们来分析一下哪些操作会引起主线程阻塞。
1.和客户端交互时的阻塞点。
键值对的增删改查操作是Redis和客户端交互的主要部分,也是Redis主线程执行的主要任务。所以复杂度高的增删改查操作肯定会阻塞Redis。在Redis中复杂度高的操作都是对集合的操作,通常时间复杂度为O(N)。所以在使用的过程中需要注意起来,例如对集合的全量查询操作HGETALL以及对集合的聚合统计操作,例如求交、并和差集。所以集合全量查询和聚合操作是Redis的一个阻塞点。
除此之外,对集合删除操作也会有潜在的阻塞风险。因为删除操作的本质是要释放键值对占用的内存空间。释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序,所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞。所以删除bigkey也是Redis的一个阻塞点。
因为清空数据库涉及到删除和释放所有的键值对。所以清空数据库也是Redis的一个阻塞点。
2.和磁盘交互时的阻塞点。
Redis是采用后台子进程的方式生成RDB快照文件,以及执行AOF日志重写操作。所以这两个操作由子进程负责执行,因此不会成为Redis的阻塞点。
但是Redis直接记录AOF日志时,会根据不同的写回策略对数据做落盘保存。
如果有大量的写操作需要记录在AOF日志中,并写回策略设置成同步写回的话,就会阻塞主线程了。所以AOF日志同步写也是Redis的一个阻塞点。
3.主从节点交互时的阻塞点
在主从集群中,主库需要生成RDB文件,并传输给从库。主库在复制的过程中,创建和传输 RDB 文件都是由后台子进程来完成的,不会阻塞主线程的执行。但是,对于从库来说,它在接收了RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库,然后需要把 RDB 文件加载到内存,这个过程的快慢和 RDB 文件的大小密切相关,RDB 文件越大,加载过程越慢,所以,加载 RDB 文件就成为了 Redis 又一个阻塞点。
所以总结一下,Redis中阻塞主线程的操作主要有以下五种。
-
集合全量查询和聚合操作。
-
bigkey 删除。
-
清空数据库。
-
AOF 日志同步写。
-
从库加载 RDB 文件。
那有什么方式可以避免阻塞式操作呢?Redis提供了异步线程机制。为了避免一些阻塞主线程的操作,Redis会启动一些子线程,然后把一些任务交给这些子线程去在后台完成,而不再由主线程来执行这些任务。下面我们来看一下哪些阻塞主线程的操作可以采用异步执行呢?
如果一个操作能被异步执行,就意味着,它并不是Redis主线程的关键路径上的操作。所谓的关键路径是指客户端把请求发送给Redis后等着Redis返回数据结果的操作。如下图所示。
主线程接收到操作1后,因为操作1并不用给客户端返回具体的数据,所以,主线程可以把它交给后台子线程来完成,同时只要给客户端返回一个“OK”结果就行。在子线程执行操作1的时候,客户端又向Redis实例发送了操作2,而此时,客户端是需要使用操作2返回的数据结果的,如果操作2不返回结果,那么,客户端将一直处于等待状态。在这个例子中,操作 1 就不算关键路径上的操作,因为它不用给客户端返回具体数据,所以可以由后台子线程异步执行。而操作 2 需要把结果返回给客户端,它就是关键路径上的操作,所以主线程必须立即把这个操作执行完。
对于 Redis 来说,读操作是典型的关键路径操作,因为客户端发送了读操作之后,就会等待读取的数据返回。而Redis 的集合全量查询和聚合操作都涉及到了读操作,所以,它们是不能进行异步操作了。
我们再来看看删除操作。删除操作并不需要给客户端返回具体的数据结果,所以不是关键路径操作。所以“bigkey 删除”和“清空数据库”,都是对数据做删除,所以我们可以用后台子线程来异步执行删除操作。
对于“AOF 日志同步写”来说,为了保证数据可靠性,Redis 实例需要保证 AOF 日志中的操作记录已经落盘,这个操作虽然需要实例等待,但它并不会返回具体的数据结果给实例。所以,我们也可以启动一个子线程来执行 AOF 日志的同步写,而不用让主线程等待 AOF 日志的写完成。
最后,我们再来看下“从库加载 RDB 文件”这个操作。从库要想对客户端提供数据存取服务,就必须把 RDB 文件加载完成。所以,这个操作也属于关键路径上的操作,我们必须让从库的主线程来执行。
对于 Redis 的五大阻塞点来说,除了“集合全量查询和聚合操作”和“从库加载 RDB 文件”,其他三个阻塞点涉及的操作都不在关键路径上,所以,我们可以使用 Redis 的异步子线程机制来实现 bigkey 删除,清空数据库,以及 AOF 日志同步写。
4.异步的子线程机制
Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。主线程通过一个链表形式的任务队列和子线程进行交互。当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始实际删除键值对,并释放相应的内存空间。因此,我们把这种异步删除也称为惰性删除(lazy free)。此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。和惰性删除类似,当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。
当你的集合中有大量的元素要删除时,我建议你使用异步删除命令 UNLINK 。而对于清空数据库来说,可以在 FLUSHDB 和 FLUSHALL 命令后加上 ASYNC 选项,这样就可以让后台子线程异步地清空数据库。
FLUSHDB ASYNC FLUSHALL AYSNC
更多硬核知识,请关注公众号"程序员学长"。