第十六章 异步机制:如何避免单线程模型的阻塞

Redis使用主线程处理网络IO和键值对操作,可能导致阻塞。阻塞点包括集合查询、bigkey删除、清空数据库、AOF日志同步和从库加载RDB。为避免阻塞,Redis4.0后引入异步子线程,执行键值对删除和数据库清空。UNLINK命令用于异步删除,FLUSHDB/FLUSHALLASYNC用于异步清空。此外,建议使用SCAN命令分批处理查询,控制主库数据量以加速从库加载。
摘要由CSDN通过智能技术生成

第十六章 异步机制:如何避免单线程模型的阻塞 ?

  • Redis 的网络 IO 和键值对读写是由主线程完成的。
  • 如果在主线程上执行的操作消耗的时间太长,就会引起主线程阻塞。

Redis 实例有哪些阻塞点 ?

与 Redis 实例交互的对象有哪些 ?

  • 客户端:网络 IO,键值对增删改查操作,数据库操作;
  • 磁盘:生成 RDB 快照,记录 AOF 日志,AOF 日志重写;
  • 主从节点:主库生成、传输 RDB 文件,从库接收 RDB 文件、清空数据库、加载 RDB 文件;
  • 切片集群实例:向其他实例传输哈希槽信息,数据迁移。

Untitled

和客户端交互时的阻塞点

  • Redis 主线程执行的主要任务是:键值对的增删改查操作,
  • 所以复杂度高的增删改查操作肯定会阻塞 Redis。

Redis 实例和客户端交互有哪些阻塞点呢 ?

  • 集合全量查询和聚合操作会阻塞 Redis
  • 删除包含大量元素的集合的操作会阻塞 Redis
  • 清空数据库操作会阻塞 Redis

为什么删除操作还会阻塞 Redis ?

  • 删除操作的本质是要释放键值对占用的内存空间。
  • 在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。
  • 这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。
  • 如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞

什么时候会释放大量内存呢 ?

删除大量键值对数据的时候,最典型的就是删除包含了大量元素的集合,也称为 bigkey 删除。

和磁盘交互时的阻塞点

  • 生成 RDB 快照文件和执行 AOF 日志重写操作由子进程完成,不阻塞主线程。
  • 如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程,耗时大概 1~2 ms。

主从节点交互时的阻塞点

  • 主库在复制的过程中,创建和传输 RDB 文件都是由子进程来完成的,不会阻塞主线程。
  • 从库接收RDB文件后,需要使用 FLUSHDB 命令清空当前数据库,这样会阻塞 从库的Redis 主线程。
  • 从库在清空当前数据库后,还需要把 RDB 文件加载到内存,这个过程的快慢和 RDB 文件的大小密切相关,RDB 文件越大,加载过程越慢,这样也会阻塞 从库的Redis 主线程。

切片集群实例交互时的阻塞点

  • 当没有 bigkey 时,切片集群的各实例在进行交互时不会阻塞主线程。
  • 如果你使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster 使用了同步迁移。

总结有哪些阻塞点 ?

  • 集合全量查询和聚合操作;
  • bigkey 删除;
  • 清空数据库;
  • AOF 日志同步写;
  • 从库加载 RDB 文件。

如果在主线程中执行这些操作,必然会导致主线程长时间无法服务其他请求。为了避免阻塞式操作,Redis 提供了异步线程机制。

所谓的异步线程机制,就是指,Redis 会启动一些子线程,然后把一些任务交给这些子线程,让它们在后台完成,而不再由主线程来执行这些任务。

使用异步线程机制执行操作,可以避免阻塞主线程。

哪些阻塞点可以异步执行 ?

异步执行 对操作的要求 ?

Untitled

  • 主线程接收到操作 1 后,因为操作 1 并不用给客户端返回具体的数据,所以,主线程可以把它交给后台子线程来完成,同时只要给客户端返回一个“OK”结果就行。
  • 在子线程执行操作 1 的时候,客户端又向 Redis 实例发送了操作 2,而此时,客户端是需要使用操作 2 返回的数据结果的,如果操作 2 不返回结果,那么,客户端将一直处于等待状态。
  • 在这个例子中,操作 1 就不算关键路径上的操作,因为它不用给客户端返回具体数据,所以可以由后台子线程异步执行
  • 而操作 2 需要把结果返回给客户端,它就是关键路径上的操作,所以主线程必须立即把这个操作执行完。

哪些阻塞点可以异步执行呢 ? 哪些算关键路径操作,哪些不算呢 ?

  • 读操作是典型的关键路径操作,因为它需要把结果返回给客户端,所以它不能进行异步操作。
  • 删除操作并不需要给客户端返回具体的数据结果,所以不算是关键路径操作,可以使用后台子线程异步执行删除操作。
  • 对于AOF同步写来说,为了保证数据可靠性,Redis 实例需要保证 AOF 日志中的操作记录已经落盘,这个操作虽然需要实例等待,但它并不会返回具体的数据结果给实例。
    • 所以,我们也可以启动一个子线程来执行 AOF 日志的同步写,而不用让主线程等待 AOF 日志的写完成。
  • 对于从库加载 RDB 文件来说,从库要想对客户端提供数据存取服务,就必须把 RDB 文件加载完成。
    • 所以,这个操作也属于关键路径上的操作,我们必须让从库的主线程来执行。

Redis 实现的异步子线程机制具体是怎么执行呢 ?

  • Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责
    • AOF 日志写操作
    • 键值对删除
    • 文件关闭的异步执行
  • 主线程通过一个链表形式的任务队列和子线程进行交互。当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。
  • 但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始实际删除键值对,并释放相应的内存空间。因此,我们把这种异步删除也称为惰性删除(lazy free)。此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。
  • 和惰性删除类似,当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。

Untitled

异步的键值对删除和数据库清空操作是 Redis 4.0 后提供的功能,Redis 也提供了新的命令来执行这两个操作:

  • 键值对删除:当你的集合类型中有大量元素(例如有百万级别或千万级别元素)需要删除时,我建议你使用 UNLINK 命令。
  • 清空数据库:可以在 FLUSHDBFLUSHALL 命令后加上 ASYNC 选项,这样就可以让后台子线程异步地清空数据库,如下所示:
FLUSHDB ASYNC
FLUSHALL AYSNC

对于 集合全量查询和聚合操作从库加载 RDB 文件 这两个无法使用异步操作来完成的阻塞点,有以下建议:

  • 集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;
  • 从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。

Redis 的写操作(例如 SET、HSET、SADD 等)是在关键路径上吗 ?

需要客户端根据业务需要来区分:

  • 如果客户端依赖操作返回值的不同,进而需要处理不同的业务逻辑,那么HSET和SADD操作算关键路径,而SET操作不算关键路径。
    • 因为HSET和SADD操作,如果field或member不存在时,Redis结果会返回1,否则返回0。而SET操作返回的结果都是OK,客户端不需要关心结果有什么不同。
  • 如果客户端不关心返回值,只关心数据是否写入成功,那么SET/HSET/SADD不算关键路径,多次执行这些命令都是幂等的,这种情况下可以放到异步线程中执行。

什么时候 Redis 才会真正的异步释放内存 ?

lazy free机制:Redis收到键值对删除和清空数据库的指令时,主线线程会把这个操作封装成一个任务,放入任务队列中,然后给客户端返回一个完成信息,但实际上,这个删除还没有执行,需要等待后台子线程从任务队列中读取到这个任务后,才开始实际删除键值对,并释放相应的内存空间。

但是 lazy-free 是4.0新增功能,默认关闭。开启这个配置后, 除了 replica-lazy-flush 之外,其他情况都只是可能去异步释放key的内存,并不是每次必定异步释放内存的。

是否会真正异步释放内存,这和key的类型、编码方式、元素数量都有关系!!!

  • 当Hash/Set底层采用哈希表存储(非ziplist/int编码存储)时,并且元素数量超过64个
  • 当ZSet底层采用跳表存储(非ziplist编码存储)时,并且元素数量超过64个
  • 当List链表节点数量超过64个(注意,不是元素数量,而是链表节点的数量,List的实现是在每个节点包含了若干个元素的数据,这些元素采用ziplist存储)

只有以上这些情况,在删除key释放内存时,才会真正放到异步线程中执行,其他情况一律还是在主线程中操作。

也就是说String(不管内存占用多大)、List(少量元素)、Set(int编码存储)、Hash/ZSet(ziplist编码存储)这些情况下的key在释放内存时,依旧在主线程中操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿小羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值