redis的网络io和键值对读写都是在主线程中完成,如果主线程上的某个操作耗时很长的话就会导致主线程堵塞。下面这张图列出了可能会导致redis堵塞的几个点。

redis 阻塞锁  redis是阻塞还是非阻塞_网络连接


redis是如何处理这几种场景避免堵塞呢?初略总结大概有这几种种方案:多线程,多进程,io多路复用,渐进式处理。

方案

场景

多线程

大键删除,AOF磁盘同步,文件删除,网络io(7.0版本)

多进程

RDB,AOF重写

IO多路复用

网络IO

渐进式处理

哈希扩容收缩rehash,槽位迁移,过期键删除,数据驱逐

IO处理
redis使用的是IO多路复用机制来避免主线程一直处于等待网络连接或命令请求。多路复用中的’多路’指的是多个网络连接,多个socket,'复用’指的是复用线程,就是说用一个线程去监听多个网络连接。可以redis是通过select,epoll,evport,kqueue这几种库来实现的。可以简单的理解,我们可以通过这几种多路复用接口告诉内核我们对哪个网络连接的什么事件(读、写)感兴趣,当这个网络连接有事件发生的时候内核会通知应用程序。如果没有网络事件发生应用程序可以去干其它事情,这样就不会因为io堵塞。redis将socket设置成非堵塞模式,也能避免主线程被堵塞。在最新7.0版本,redis IO是用的多线程,主线程还是只有一个,但是可以有多个io线程。

磁盘交互
redis在执行持久化的时候,需要将内存中的数据保存到磁盘中。由于磁盘操作比较慢,redis是用子进程去执行持久化。但如果是采用AOF持久化方案还是会有堵塞主线程的风险。redis记录AOF日志时会根据不同的会写策略对数据做磁盘同步。之所以需要磁盘同步,是因为在Linux/Unix系统中,在文件或数据处理过程中一般先放到内存缓冲区中,等到适当的时候再写入磁盘,以提高系统的运行效率。就因为这个原因会给redis带来数据丢失的风险,如果在数据还没写入磁盘的时间突然断电数据就丢失了。所以redis需要用sync函数强制将内存缓冲区中的数据立即写入磁盘中。redis提供的三种AOF磁盘同步策略:always-每次执行,everysecond-每秒执行,No-操作系统说了算。如果选的是always,则每条命令都会执行磁盘同步,这会导致性能下降。如果是everysecond,是每秒钟执行一次,redis为了避免主线程堵塞,是吧磁盘同步放到后台任务去执行,即是采用多线程的解决方案。

客户端交换
这里讨论的客户端交换这部分主要指客户端对数据库键值对增删改查,这也是redis的主要任务。因此如果这部分耗时太久的话是会堵塞redis主线程。耗时过久主要是由于命令的操作复杂度高因此的,不如说使用MSET命令去添加一百万个key,毫无疑问会堵塞主线程。
如果一个命令的操作复杂度是O(n),那就意味着这个命令有堵塞风险,一般集合全量查询和聚合操作的复杂度都比较高。
另一种是情况是bigkey大键删除也有潜在的堵塞风险,因为删除需要释放键值对占用的内存空间,将内存归还给内存分配系统。redis用的内存分配系统是jemalloc,一般内存分配系统都会维护一个复杂的链表系统,申请释放内存需要消耗一定的时间。redis为了解决这个问题,删除大键是放在后台线程去执行。当删除一个键的时候,redis会大致衡量一下所需的时间,如果redis觉得耗时比较大的话就把这个删除操作扔给后台线程去执行。
最后一个,清空数据库flushdb,flushall,毫无疑问这也是一个堵塞点。

主从交互
主从模式中,主库需要生成一个rdb文件然后发送给从库,这部分都是在子进程中进行,不会堵塞主线程。从库在收到rdb文件的时候会先清空当前数据库,这个操作上面讲到是会堵塞主线程的。从库清空完数据库后,要从rdb文件中加载数据,这个也是会堵塞主线程的。

集群交互
在集群模式中,redis会将16384个槽位分派给各个节点,当需要进行负载均衡或者增删节点的时候可能需要进行槽位迁移:将一个节点的槽位指派给其它节点。为了避免槽位迁移过程堵塞主线程,槽位迁移不是一次完成的,而是分为多次渐进式完成。redis槽位迁移是通过redis-trib来完成,简单来讲就是redis-trib首先会或者源节点的准备迁移的那个槽位上的所有键名称,这个是通过getkeysinslot命令来完成,然后向源节点发送命令,让源节点对这个槽位的每个key进行迁移,也就是说每次只迁移一个key,避免堵塞。但是即使这样,在迁移一个大键的时候,还是可能会堵塞主线程。
redis自身操作
除了上面这些交互场景,redis也会有自身的一些操作可能会堵塞主线程。
第一个就是rehash哈希自扩容收缩,当一个hash表的负载因子太大或者太小的时候,redis会对hash表进行一些调整。负载因子太大就是自动扩容,太小就自动收缩。这些操作是要对hash表的所有对象进行迁移,操作负载度会比较高,如果一次性迁移完成也会堵塞主线程,所以redis采用的是渐进式rehash,每次只迁移一个哈希槽位,即使槽位再多也是总有一天会完成的!
还有另一个场景是过期键删除,键的删除需要释放内存等,如果过期键比较多的时候,删除可能会比较耗CPU时间,对CPU不友好。假设现在不缺内存,还有大量的命令等着去处理,那么将CPU时间消耗在删除一些已经不用了和当前任务已经没有关系的键上面是不合理的。为了解决这个问题,redis提供两种过期键删除策略:lazyfree惰性删除,过期删除。
惰性删除指的是只在取出键的时候才会检查键是否过期,如果过期就删除,这样对CPU比较友好,但是对内存不友好,会造成内存泄漏。
过期马上删除占用CPU,而惰性删除浪费内存,那么定期删除就是这两种方案的折中方案。
定期删除是指每隔一段时间就执行一次过期键删除,还要限制单次删除操作的执行时长来减少CPU时间消耗避免堵塞主线程。这种思想和渐进式rehash和渐进式槽位迁移是一样的,redis定期删除每次检测都会随机抽出一小部分(25个)键,然后检查是否过期,如果过期就会删除。