首页
学习
活动
专区
圈层
工具
发布
清单首页Redis文章详情

异步编程规避Redis的阻塞

1 简介

Redis重视影响Redis性能的因素,如:

  • 命令操作
  • 系统配置
  • 关键机制
  • 硬件配置 ...

要尽可能避免性能异常场景,还要做好异常应对方案。影响Redis性能的潜在风险:

  • Redis内部的阻塞式操作
  • CPU核和NUMA架构的影响
  • Redis关键系统配置
  • Redis内存碎片
  • Redis缓冲区

本文研究Redis内部的阻塞式操作及应对方案。

Redis的网络I/O和KV对读写都由主线程完成。若在主线程执行操作耗时太长,就会引起主线程阻塞。但Redis既有服务客户端请求的键值对增删改查操作,也有保证可靠性的持久化操作,还有主从复制时的数据同步操作。哪些会引起阻塞?

2 Redis阻塞风险点

Redis要和不同对象交互,有不同操作:

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

2.1 客户端交互

Redis使用I/O多路复用,避免主线程一直处在等待网络连接或请求到来的状态,所以,网络I/O并非导致Redis阻塞因素。

2.1.1 集合全量查询和聚合操作

KV对的crud操作是Redis和客户端主要交互,也是Redis主线程执行的主要任务。复杂度高crud操作势必阻塞Redis。

最基本标准:看操作复杂度是否O(N)。Redis涉及集合的操作复杂度通常O(N):

  • 集合元素全量查询操作,如HGETALL、SMEMBERS
  • 集合的聚合统计操作,如交、并差集

2.1.2 删除大key

集合自身的删除也可能阻塞。

Q:不就直接数据删除,咋阻塞主线程?

A:删除本质是释放KV对占用内存空间。

释放内存只是第一步,为高效管理内存,应用程序释放内存时,os要把释放掉的内存块插入一个空闲内存块的链表,以便后续管理和再分配。这过程耗时,且会阻塞当前释放内存的应用程序。

所以,若突然释放大量内存,空闲内存块链表操作时间就会增加,导致Redis主线程阻塞。

啥时释放大量内存?

最常见于删除含大量元素的集合,即删除bigkey。不同元素数量集执行删除耗时:

集合类型

10万(8字节)

100万(8字节)

10万(128字节)

100万(128字节)

Hash

50ms

962ms

91ms

1980ms

List

25ms

133ms

29ms

283ms

Set

42ms

821ms

75ms

1347ms

Sorted Set

53ms

809ms

61ms

991ms

  • 当元素数量从10w到100w,集合类型删除时间增长幅度从5倍上升到近20倍
  • 集合元素越大,删除耗时越长
  • 当删除有100w个元素的集合时,最大删除时间绝对值已达1.98s(Hash类型)。Redis响应时间一般在微秒级,这不可避免严重阻塞主线程

2.1.3 清空数据库

Redis数据库级操作:清空数据库,如FLUSHDB、FLUSHALL也是重大阻塞风险,涉及删除、释放所有KV对。

2.2 磁盘交互阻塞

2.2.1 AOF日志同步写

磁盘I/O费时费力,Redis开发者早就设计为:

  • 子进程生成RDB
  • AOF日志重写

都由子进程负责执行,慢速的磁盘I/O就不阻塞主线程。

但Redis直接记录AOF日志时,会根据不同写回策略对数据做落盘保存。

一个同步写盘操作耗时大约1~2ms,若大量写操作需记录在AOF日志,并同步写回,就会阻塞主线程。

2.3 主从节点交互阻塞

2.3.1 从库加载RDB文件

主从集群中的主库需:

  • 生成RDB文件
  • 并传输给从库

主库在复制过程,创建、传输RDB都由子进程完成,不阻塞主线程。

但从库,接收RDB文件后,需用FLUSHDB命令清空当前数据库,恰好撞车三大阻塞点。从库清空当前数据库后,还要把RDB文件载入内存,RDB文件越大,加载越慢。

2.3.2 分片集群实例交互阻塞

  • 部署Redis Cluster时,每个Redis实例上分配的哈希槽信息,需在不同实例间传递 不过,哈希槽信息量不大
  • 当需负载均衡或有实例数变化时,数据会在不同实例间迁移 而数据迁移是渐进式执行

所以,一般这两类操作对Redis主线程阻塞影响不大。

但若使用Redis Cluster,且同时正好迁移大key,就会阻塞主线程,因Redis Cluster使用的同步迁移。

当无大key时,分片集群的各实例在进行交互时一般不会阻塞主线程。

在主线程中执行以上操作,势必导致主线程长时间无法服务其它请求。

为避免阻塞式操作,Redis提供异步线程机制:Redis会启动一些子线程,把一些任务移交子线程,让它们在后台处理。使用异步线程机制执行操作,可以避免阻塞主线程。

以上这些阻塞式操作可以被异步执行吗?

3 可异步执行的阻塞点

在分析阻塞式操作的异步执行的可行性前,先了解异步执行对操作的要求。

若一个操作能被异步执行,说明它不是Redis主线程关键路径上的操作。

3.1 关键路径操作

客户端把请求发给Redis后,等Redis返回数据结果:

  • 主线程接收到操作1后,由于操作1无需给客户端返回具体数据,所以,主线程可将其移交给后台子线程处理,同时只需给客户端返回“OK”。 操作1就不属关键路径操作,因其不用给客户端返回具体数据,所以可由后台子线程异步执行
  • 子线程执行操作1时,客户端又向Redis实例发送操作2,而此时,客户端需使用操作2返回的具体数据结果。若操作2不返回结果,则客户端将一直处等待状态。 该操作需把结果返给客户端,所以是关键路径操作,主线程须立即执行完该操作。

那Redis的写操作(如SET,HSET,SADD)属于关键路径吗?这需要客户端根据业务需要区分:

  • 若客户端依赖操作返回值的不同而处理不同业务逻辑,则HSET、SADD算关键路径,而SET操作不算关键路径

因为HSET和SADD操作,若field或member不存在,Redis返回1,否则返0。而SET操作返回的结果都是OK

  • 若客户端不关心返回值,只关心数据是否写成功,则SET/HSET/SADD都不算关键路径,多次执行这些命令都是幂等的,这时可放到异步线程
  • 若Redis设置maxmemory,但未设置淘汰策略,这三个操作也都算关键路径

因为若Redis内存超过maxmemory,再写入数据时,Redis返回的结果是OOM error,这种情况下,客户端需要感知有错误发生才行

3.2 各阻塞点分析

3.2.1 集合全量查询和聚合操作

Redis读都是关键路径操作,因为客户端发起读请求后,就会等待返回读取数据,再处理后续。所以,涉及读操作,无法异步!

推荐使用SCAN命令,分批读取数据,再在客户端进行聚合计算。

3.2.2 删除操作

无需给客户端返具体数据,不算关键路径操作。

“大K删除”、“清空数据库”同理,都可用后台子线程异步执行。

3.2.3 AOF日志同步写

为保证数据可靠性,Redis实例需保证AOF日志中的操作记录已落盘,这操作虽需实例等待,但不会返回具体数据结果给实例。所以,可使用一个子线程执行。

3.2.4 从库加载RDB文件

从库想对客户端提供数据存取服务,须将RDB文件加载完成。所以,这也属于关键路径操作,须让从库的主线程执行。把主库数据量大小控制在2~4GB左右,以保证RDB文件能以较快的速度加载。

综上,可使用Redis异步子线程机制实现大K删除,清空数据库及AOF日志同步写。

本文已收录在Github关注我,紧跟本系列专栏文章,咱们下篇再续!

  • 🚀 魔都架构师 | 全网30W技术追随者
  • 🔧 大厂分布式系统/数据中台实战专家
  • 🏆 主导交易系统百万级流量调优 & 车联网平台架构
  • 🧠 AIGC应用开发先行者 | 区块链落地实践者
  • 🌍 以技术驱动创新,我们的征途是改变世界!
  • 👉 实战干货:编程严选网
下一篇
举报
领券