Redis 被广泛应用的原因是因为它支持高性能访问。所以,我们要重视所有可能影响 Redis 性能的因素(如命令操作、系统配置、关键机制、硬件配置等)。
影响 Redis 性能的 5 大方面的潜在因素分别是:
Redis 实例要和许多对象进行交互,这些不同的交互就会涉及不同的操作,我们来看看客户端和 Redis 实例交互的对象,以及交互时会发送的操作。
网络 IO 有时候会比较慢,但是 Redis 使用了 IO 多路复用机制
,避免了主线程一直处在等待网络连接或者请求到来的状态,所以网络 IO 不是导致 Redis 阻塞的因素。
键值对的增删改查操作是 Redis 和 客户端交互的主要部分,也是 Redis 主线程执行的主要任务。所以,复杂度高的增删改查操作会阻塞 Redis。
判断操作复杂度有一个最基本的标准,就是看操作的复杂度是否为
O(N)
.
Redis 中涉及集合的操作复杂度通常为 O(N),所以我们要重视起来。例如集合元素全量查询 HGETALL、SMEMBERS,以及集合的统计聚合操作,例如求交、并和差集。这些操作可以作为 Redis 的第一个阻塞点:集合的全量查询和聚合操作。
另外,集合自身的删除操作,同样也有潜在的阻塞风险。这是因为删除不仅仅只是把数据删除,还要释放键值对占用的内存空间。
你可以不要小瞧内存释放的过程。释放内存只是第一步,为了高效管理内存空间,在应用程序释放时,操作系统需要把释放掉的内存块插入一个
空间内存块的链表
,以便后续进行管理和再分配。这个过程本身需要一定的时间,而且会阻塞当前释放内存的应用程序,所以,如果一下子释放了大量的内存,空闲内存块链表
的操作时间就会增加,相应地就会造成 Redis 主线程的阻塞。
什么时候会释放大量内存呢? 其实就是在删除大量键值对数据的时候,最典型的就是删除了包含了大量元素的集合,也称为 bigkey
删除。下图是测试了不同元素数据的集合在进行删除操作时所消耗的时间:
根据压测,可以得出结论:
很显然,Redis 的第二个阻塞点:bigkey 删除操作。
删除操作对 Redis 实例性能的负面影响很大,而且在实际业务开发时很容易被忽略,所以移动要重视它
。
既然频繁你删除键值对都是潜在的阻塞点,那么,Redis 的数据库级别操作中,清空数据库(如 FLUSHDB 和 FLUSHALL 操作)必然也是一个潜在的阻塞风险,因为它设计到删除和释放所有的键值对。所以,Redis的第三个阻塞点:清空数据库。
磁盘 IO 一般都是比较耗时费力的,需要重点关注。不过,Redis 开发者早就认识到磁盘 IO 会带来阻塞,所以就把 Redis 设计为采用子进程的方式生成 RDB 快照文件,以及执行 AOF 日志重写操作。这样一来,这两个操作由子进程负责执行,慢速的磁盘 IO 就不会阻塞主线了。
但是,Redis 直接记录 AOF 日志时,会根据不同的写回策略对数据做落盘保存。一个 AOF 同步写
磁盘的操作的耗时大约是 1~2 ms,如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程了。这就是 Redis 的第四个阻塞点: AOF 日志荣同步写。
主从集群中,主库需要生成 RDB 文件,并传输给从库。主库在复制过程中,创建和传输 RDB 文件都是由子进程来完成,不会阻塞主线程。但是对于从库来说,它在接收了 RDB 文件后,需要使用 LPUSHDB 命令清空当前数据库,这就正好是刚刚分析的第三个阻塞点。
此外,从库在情况当前数据库后,还需要把 RDB 文件加载到内存,这个过程的快慢和 RDB 文件的大小密切相关, RDB 文件越大,加载过程越慢,所以 加载 RDB 文件就称为了 Redis 的第五个阻塞点。
当我们部署 Redis 切片集群时,每个 Redis 实例上分配的哈希槽信息需要在不同实例间进行传输,同时,当需要进行负载均衡或者实例增删时,数据会在不同的实例间进行迁移。不过哈希槽信息量不大,而数据迁移是渐进式执行的,所以一般来说,这两类操作对 Redis 主线程的阻塞风险不大。
不过,如果你使用了 Redis Cluster
方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster
使用了同步迁移。所以,你要知道,当没有 bigkey 时,切片集群的各实例在进行交互时不会阻塞主线程。
好了,经过上面分析 Redis 的各种关键操作,总结下刚刚找到的阻塞点:
如果在主线程执行这些操作,必然会导致主线程的阻塞。为了避免阻塞式操作,Redis 提供了异步线程机制。所谓异步线程机制,就是指 Redis 会启动一些子线程,然后把一些任务交给这些子线程,让它们在后台完成,而不再由主线程来执行这些任务。使用异步线程机制执行操作,可以避免阻塞主线程。
不过,这个时候,问题来了:这五大阻塞式操作都可以被异步执行吗?
如果一个操作能被异步执行,就意味着它不是 Redis 主线程的关键路径上的操作。关键路径上的操作就是,客户端把请求发给 Redis 后,等着 Redis 返回数据结构的操作。
在这个例子中,操作 1 就不算关键路径上的操作,所以可以有后台子线程异步执行。而操作 2 需要把结果返回给客户端,它就是关键路径上的操作,所以主线程立即把这个操作执行完。
对于 Redis 来说,读操作是典型的关键路径操作,因为客户端发送了读操作之后,就会等待读取数据的返回。而 Redis 的第一个阻塞点 “集合全量查询和聚合操作”
都涉及到了读操作,所以它是不能进行异步操作了。
删除操作,并不需要给客户端返回具体的数据结果,所以不算关键路径操作。而我们刚才总结的 第二个阻塞点 “bigkey 删除”
,和第三个阻塞点 “清空数据库”
都是对数据做删除,并不在关键路径上。因此可以使用后台子线程来异步执行删除操作
。
最后,我们看下“从库加载 RDB 文件”这个阻塞点
。从库想要对客户端提供数据存取服务,就必须把 RDB 文件加载完成,所以,这个操作也属于关键路径上的操作
,必须让从库的主线程来执行。
Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由他们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。
主线程通过一个链表形式的任务队列和子线程进行交互。当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。
但实际上,这个时候删除还没执行,等到后台子线程从任务队列中读取数据,才实际删除键值对,并释放响应的内存空间。因为,我们把这种异步删除也称为惰性删除(lazy free)。此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。
和惰性删除类似,当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。
下图展示了 Redis 中异步子线程执行机制。
不过,有个地方需要注意下,异步键值对删除和情况数据库操作是 Redis 4.0 后提供的功能
,Redis 也提供了新的命令来执行这两个操作。
在 FLUSHDB 和 FLUSHALL 命令后机上 AYSNC 选项,这样可以让后台子线程异步地清空数据库
。FLUSHDB AYSNC
FLUSHALL AYSNC
本章,我们学习了 Redis 实例允许时的 4 大类交互对象:客户端、磁盘、主从库实例、切片集群实例。基于这 4 大类交互对象,我们梳理了会导致 Redis 性能受损的 5 大阻塞点,包括集合全量查询和聚合操作、bigkey 删除、清空数据库、AOF 日志同步写,以及从库加载 RDB 文件。
在这 5 大阻塞点中,bigkey 删除、情况数据库、AOF 日志同步写不属于关键路径操作,可以使用异步子线程机制来完成。 Redis 在运行时会创建三个子线程,主线程会通过一个任务队列和三个子线程进行交互。子线程会根据任务的具体类型,来执行响应的异步操作。
不过 异步删除操作是 Redis 4.0 以后才有的功能,如果你使用的是 4.0 之前的版本,当你遇到 bigkey 删除时,给你个小建议:
先使用集合类型提供的 SCAN 命令读取数据,然后再次进行删除。因为 SCAN 命令可以每次只读取一部分数据并删除,这样可以避免一次性删除大量 key 给主线程带来阻塞。
例如,对于 Hash 类型的 bigkey 删除,你可以使用 HSCAN 命令,每次从 Hash 集合中获取一部分键值对(如 200 个),再使用 HDEL 删除这些键值对,这样就可以把删除压力分摊到多次操作中,那么,每次删除操作的耗时就不会太长,也就不会阻塞主线程了。
最后,集合全量查询和聚合操作、从库加载 RDB 文件是在关键路径上,无法使用异步操作来完成,对于这两个阻塞点的小建议:
可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算
。把主库的数据量大小控制在 2~4GB 左右
,以保证 RDB 文件能以较快的速度加载。