Redis 管道
提示
来自deepseek解释
原文链接:https://redis.io/docs/latest/develop/using-commands/pipelining/
Redis 管道是一种通过一次性发出多个命令而不等待每个单独命令的响应来提高性能的技术。大多数 Redis 客户端都支持管道。本文档描述了管道旨在解决的问题以及管道在 Redis 中的工作原理。
请求/响应协议与往返时间(RTT)
Redis 是一个 TCP 服务器,采用客户端-服务器模型和所谓的请求/响应协议。
这意味着通常一个请求通过以下步骤完成:
- 客户端向服务器发送查询,并通常以阻塞方式从套接字读取服务器响应。
- 服务器处理命令并将响应发送回客户端。
因此,例如,四个命令的序列是这样的:
- 客户端: INCR X
- 服务器: 1
- 客户端: INCR X
- 服务器: 2
- 客户端: INCR X
- 服务器: 3
- 客户端: INCR X
- 服务器: 4
客户端和服务器通过网络链路连接。这样的链路可能非常快(回环接口)或非常慢(通过互联网建立的连接,两个主机之间有许多跳转)。无论网络延迟如何,数据包从客户端到服务器、再从服务器返回到客户端以携带回复都需要时间。
这段时间称为 RTT(往返时间)。很容易看出,当客户端需要连续执行许多请求时(例如向同一个列表添加许多元素,或使用许多键填充数据库),这会对性能产生多大影响。例如,如果 RTT 时间为 250 毫秒(在互联网上非常慢的链路情况下),即使服务器每秒能够处理 10 万次请求,我们最多也只能每秒处理 4 次请求。
如果使用的是回环接口,RTT 要短得多,通常低于毫秒,但如果您需要连续执行多次写入,即使这样也会累积很多。
幸运的是,有一种方法可以改善这种用例。
Redis 管道
请求/响应服务器可以实现为即使客户端尚未读取旧响应,也能够处理新请求。这样,就可以向服务器发送多个命令而完全不用等待回复,最后在一步中读取所有回复。
这就是所谓的管道,这是一种已经广泛使用了几十年的技术。例如,许多 POP3 协议实现已经支持此功能,极大地加速了从服务器下载新电子邮件的过程。
Redis 从早期就支持管道,因此无论您运行哪个版本,都可以在 Redis 中使用管道。这是一个使用原始 netcat 工具的示例:
$ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG这次我们不需要为每次调用支付 RTT 成本,而只为三个命令支付一次。
明确地说,使用管道时,我们第一个示例的操作顺序将如下:
- 客户端: INCR X
- 客户端: INCR X
- 客户端: INCR X
- 客户端: INCR X
- 服务器: 1
- 服务器: 2
- 服务器: 3
- 服务器: 4
重要提示:虽然客户端使用管道发送命令,但服务器将被迫将回复排队,使用内存。因此,如果您需要使用管道发送大量命令,最好将它们分批发送,每批包含合理数量,例如 1 万条命令,读取回复,然后再发送另外 1 万条命令,依此类推。速度几乎相同,但额外使用的内存最多仅为排队这些 1 万条命令回复所需的内存量。
不仅仅是 RTT 的问题
管道不仅仅是减少与往返时间相关的延迟成本的一种方式,它实际上大大提高了在给定 Redis 服务器上每秒可以执行的操作数量。这是因为在不使用管道的情况下,从访问数据结构和生成回复的角度来看,服务每个命令非常便宜,但从执行套接字 I/O 的角度来看却非常昂贵。这涉及调用 read() 和 write() 系统调用,这意味着从用户态进入内核态。上下文切换是一个巨大的速度惩罚。
当使用管道时,多个命令通常通过一次 read() 系统调用读取,多个回复通过一次 write() 系统调用发送。因此,每秒执行的总查询数最初随着管道变长而几乎线性增加,最终达到未使用管道时的 10 倍,如下图所示。

实际代码示例
在下面的基准测试中,我们将使用支持管道的 Redis Ruby 客户端来测试管道带来的速度提升:
require 'rubygems'
require 'redis'
def bench(descr)
start = Time.now
yield
puts "#{descr} #{Time.now - start} seconds"
end
def without_pipelining
r = Redis.new
10_000.times do
r.ping
end
end
def with_pipelining
r = Redis.new
r.pipelined do |rp|
10_000.times do
rp.ping
end
end
end
bench('without pipelining') do
without_pipelining
end
bench('with pipelining') do
with_pipelining
end在我的 Mac OS X 系统上运行上述简单脚本,通过回环接口执行,管道提供的改进最小,因为 RTT 已经很低了,结果如下:
without pipelining 1.185238 seconds
with pipelining 0.250783 seconds如您所见,使用管道后,我们将传输速度提高了五倍。
管道与脚本
使用自 Redis 2.6 起可用的 Redis 脚本,许多管道的使用场景可以通过在服务器端执行大量工作的脚本来更高效地处理。脚本的一大优势是它能够以最小的延迟读取和写入数据,使得读取、计算、写入等操作非常快速(管道在这种情况下无济于事,因为客户端需要读取命令的回复才能调用写入命令)。
应用程序有时也可能希望在管道中发送 EVAL 或 EVALSHA 命令。这完全是可行的,Redis 通过 SCRIPT LOAD 命令显式支持它(它保证 EVALSHA 可以在没有失败风险的情况下被调用)。
附录:为什么即使在回环接口上,忙循环也很慢?
即使有了本文档中介绍的所有背景,您可能仍然会想知道,为什么像下面这样的 Redis 基准测试(伪代码)即使在回环接口上执行,服务器和客户端运行在同一台物理机器上时,速度也很慢:
FOR-ONE-SECOND:
Redis.SET("foo","bar")
END毕竟,如果 Redis 进程和基准测试都运行在同一台机器上,难道不只是在内存中将消息从一个地方复制到另一个地方,而不涉及任何实际延迟或网络吗?
原因是系统中的进程并不总是在运行,实际上是内核调度器让进程运行。因此,例如,当基准测试被允许运行时,它会从 Redis 服务器读取回复(与上次执行的命令相关),并写入一个新命令。该命令现在位于回环接口缓冲区中,但为了被服务器读取,内核应该调度服务器进程(当前在系统调用中被阻塞)运行,依此类推。因此,实际上,由于内核调度器的工作方式,回环接口仍然涉及类似网络的延迟。
基本上,在对网络服务器进行性能测量时,忙循环基准测试是最愚蠢的做法。明智的做法是避免以这种方式进行基准测试。