15-原理 4:雷厉风行 —— 管道
课程
1
开篇:授人以鱼不若授人以渔 —— Redis 可以用来做什么?
学习时长: 5分21秒
2
基础:万丈高楼平地起 —— Redis 基础数据结构
上次学到
学习时长: 16分14秒
3
应用 1:千帆竞发 —— 分布式锁
学习时长: 7分47秒
4
应用 2:缓兵之计 —— 延时队列
学习时长: 8分9秒
5
应用 3:节衣缩食 —— 位图
学习时长: 8分52秒
6
应用 4:四两拨千斤 —— HyperLogLog
学习时长: 14分17秒
7
应用 5:层峦叠嶂 —— 布隆过滤器
学习时长: 17分54秒
8
应用 6:断尾求生 —— 简单限流
学习时长: 4分37秒
9
应用 7:一毛不拔 —— 漏斗限流
学习时长: 7分22秒
10
应用 8:近水楼台 —— GeoHash
学习时长: 7分52秒
11
应用 9:大海捞针 —— Scan
学习时长: 8分42秒
12
原理 1:鞭辟入里 —— 线程 IO 模型
学习时长: 4分1秒
13
原理 2:交头接耳 —— 通信协议
学习时长: 3分34秒
14
原理 3:未雨绸缪 —— 持久化
学习时长: 5分27秒
15
原理 4:雷厉风行 —— 管道
学习时长: 3分51秒
16
原理 5:同舟共济 —— 事务
学习时长: 6分36秒
17
原理 6:小道消息 —— PubSub
学习时长: 7分7秒
18
原理 7:开源节流 —— 小对象压缩
学习时长: 7分14秒
19
原理 8:有备无患 —— 主从同步
学习时长: 4分9秒
20
集群 1:李代桃僵 —— Sentinel
学习时长: 3分52秒
21
集群 2:分而治之 —— Codis
学习时长: 7分28秒
22
集群 3:众志成城 —— Cluster
学习时长: 8分38秒
23
拓展 1:耳听八方 —— Stream
学习时长: 13分40秒
24
拓展 2:无所不知 —— Info 指令
学习时长: 4分4秒
25
拓展 3:拾遗补漏 —— 再谈分布式锁
学习时长: 2分18秒
26
拓展 4:朝生暮死 —— 过期策略
学习时长: 2分21秒
27
拓展 5:优胜劣汰 —— LRU
学习时长: 4分34秒
28
拓展 6:平波缓进 —— 懒惰删除
学习时长: 2分13秒
29
拓展 7:妙手仁心 —— 优雅地使用 Jedis
学习时长: 6分35秒
30
拓展 8:居安思危 —— 保护 Redis
学习时长: 2分19秒
31
拓展 9:隔墙有耳 —— Redis 安全通信
学习时长: 6分34秒
32
拓展 10:法力无边 —— Redis Lua 脚本执行原理
学习时长: 9分24秒
33
拓展 11:短小精悍 —— 命令行工具的妙用
学习时长: 9分21秒
34
源码 1:丝分缕析 —— 探索「字符串」内部
学习时长: 5分20秒
35
源码 2:循序渐进 —— 探索「字典」内部
学习时长: 7分24秒
36
源码 3:挨肩迭背 —— 探索「压缩列表」内部
学习时长: 10分42秒
37
源码 4:风驰电掣 —— 探索「快速列表」内部
学习时长: 3分49秒
38
源码 5:凌波微步 —— 探索「跳跃列表」内部
学习时长: 9分57秒
39
源码 6:破旧立新 —— 探索「紧凑列表」内部
学习时长: 2分42秒
40
源码 7:金枝玉叶 —— 探索「基数树」内部
学习时长: 5分36秒
41
源码 8:精益求精 —— LFU vs LRU
学习时长: 8分4秒
42
源码 9:如履薄冰 —— 懒惰删除的巨大牺牲
学习时长: 9分53秒
43
源码 10:跋山涉水 —— 深入字典遍历
学习时长: 9分24秒
44
源码 11:见缝插针 —— 探索 HyperLogLog 内部
学习时长: 13分3秒
45
尾声:百尺竿头 —— 继续深造指南
学习时长: 2分32秒
juejin_logo copyCreated with Sketch.

原理 4:雷厉风行 —— 管道

大多数同学一直以来对 Redis 管道有一个误解,他们以为这是 Redis 服务器提供的一种特别的技术,有了这种技术就可以加速 Redis 的存取效率。但是实际上 Redis 管道 (Pipeline) 本身并不是 Redis 服务器直接提供的技术,这个技术本质上是由客户端提供的,跟服务器没有什么直接的关系。下面我们对这块做一个深入探究。

Redis 的消息交互

当我们使用客户端对 Redis 进行一次操作时,如下图所示,客户端将请求传送给服务器,服务器处理完毕后,再将响应回复给客户端。这要花费一个网络数据包来回的时间。

如果连续执行多条指令,那就会花费多个网络数据包来回的时间。如下图所示。

回到客户端代码层面,客户端是经历了写-读-写-读四个操作才完整地执行了两条指令。

现在如果我们调整读写顺序,改成写—写-读-读,这两个指令同样可以正常完成。

两个连续的写操作和两个连续的读操作总共只会花费一次网络来回,就好比连续的 write 操作合并了,连续的 read 操作也合并了一样。

这便是管道操作的本质,服务器根本没有任何区别对待,还是收到一条消息,执行一条消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省 IO 时间。管道中指令越多,效果越好。

管道压力测试

接下来我们实践一下管道的力量。

Redis 自带了一个压力测试工具redis-benchmark,使用这个工具就可以进行管道测试。

首先我们对一个普通的 set 指令进行压测,QPS 大约 5w/s。

> redis-benchmark -t set -q
SET: 51975.05 requests per second

我们加入管道选项-P参数,它表示单个管道内并行的请求数量,看下面P=2,QPS 达到了 9w/s。

> redis-benchmark -t set -P 2 -q
SET: 91240.88 requests per second

再看看P=3,QPS 达到了 10w/s。

SET: 102354.15 requests per second

但如果再继续提升 P 参数,发现 QPS 已经上不去了。这是为什么呢?

因为这里 CPU 处理能力已经达到了瓶颈,Redis 的单线程 CPU 已经飙到了 100%,所以无法再继续提升了。

深入理解管道本质

接下来我们深入分析一个请求交互的流程,真实的情况是它很复杂,因为要经过网络协议栈,这个就得深入内核了。

上图就是一个完整的请求交互流程图。我用文字来仔细描述一遍:

  1. 客户端进程调用write将消息写到操作系统内核为套接字分配的发送缓冲send buffer
  2. 客户端操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路由」送到服务器的网卡。
  3. 服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer
  4. 服务器进程调用read从接收缓冲中取出消息进行处理。
  5. 服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer
  6. 服务器操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路由」送到客户端的网卡。
  7. 客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer
  8. 客户端进程调用read从接收缓冲中取出消息返回给上层业务逻辑进行处理。
  9. 结束。

其中步骤 5~8 和 1~4 是一样的,只不过方向是反过来的,一个是请求,一个是响应。

我们开始以为 write 操作是要等到对方收到消息才会返回,但实际上不是这样的。write 操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间来,这个就是写操作 IO 操作的真正耗时。

我们开始以为 read 操作是从目标机器拉取数据,但实际上不是这样的。read 操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就需要等待数据到来,这个就是读操作 IO 操作的真正耗时。

所以对于value = redis.get(key)这样一个简单的请求来说,write操作几乎没有耗时,直接写到发送缓冲就返回,而read就会比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开销

而对于管道来说,连续的write操作根本就没有耗时,之后第一个read操作会等待一个网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作直接就可以从缓冲拿到结果,瞬间就返回了。

小结

这就是管道的本质了,它并不是服务器的什么特性,而是客户端通过改变了读写的顺序带来的性能的巨大提升。

留言
Ctrl + Enter
全部评论(109)
cwlopq的头像
删除
感觉很多人混淆了redis命令中的set(写)、get(读),和对系统缓冲区的write(写)和read(读),导致在理解命令执行顺序时出现混乱!这也是老钱没有描述清楚的地方!
点赞
回复
柏油的头像
删除
JAVA后端
管道本质是打包批量处理,从服务端来看只当作一次正常客户端请求,所以,管道中的命令也不能太多(一般不超过100,当然,如果有特别慢的命令还得另算),否则该请求执行较慢会影响其他请求(目前命令处理还是单线程)
点赞
回复
用户6533577339797的头像
删除
使用管道技术可以显著提升Redis处理命令的速度,其原理就是将多条命令打包,多个请求打包一起发送,多个响应打包一起返回。因此只需要一次网络开销,在服务器端和客户端各一次read()和write()系统调用,以此来节约时间。
1
回复
BinK_1783的头像
删除
没心没肺,欢乐加倍,可可爱爱,值得期待
点赞
回复
大表哥酱的头像
删除
某个客户端的命令还没发送的时候恰好别的客户端发送了修改相同key的命令,这时候怎么算
点赞
1
删除
那你再改一遍 也没什么影响啊............
点赞
回复
我在火星写代码的头像
删除
后端开发
不使用管道的情况下,每发送一个请求就要等这个请求的响应后才继续发送另外一个请求。
使用管道的话,管道内的多个请求不必相互等待,一起发送给服务端。
9
回复
我在火星写代码的头像
删除
后端开发
在tcp链接断开的时候继续写入数据,就报这个错误
点赞
回复
L_lanson的头像
删除
管道应该类似于批量操作吧,只是我们可以通过编码的方式改变读写操作的顺序来减少网络开销。
3
回复
zyfsuzy的头像
删除
研发工程师 @ 百度
两个特点决定了应用场景必须有以下特点:
由于是批量流水线发送,所以每个请求之间需要无状态,即后请求不依赖前请求的响应结果
由于一次返回响应,所以响应中可能会存在有些请求命令执行成功,有些失败。所以应用场景必须能够容忍数据处理错误或者数据丢失的风险,或者能够接受通过其他机制弥补丢失或者错误的数据方式
10
回复
zyfsuzy的头像
删除
研发工程师 @ 百度
我记得管道操作要求命令之间不能有数据依赖关系存在吧
2
回复
孟德的头像
删除
java后端开发工程师 @ 飞步科技
管道相当于流水线
1
回复
Wilder70129的头像
删除
学生
使用管道是否能解决并发问题呢
点赞
回复
more_money的头像
删除
make money @ money make
管道可以看作执行的原子性么?
点赞
4
删除
不能
点赞
回复
删除
为什么?因为可能无序么?
不能
点赞
回复
查看更多回复
崔世峰的头像
删除
JAVA软件工程师
Jedis java.net.SocketException: Broken pipe (Write failed)
能说一下这个问题的本质吗?,我虽然理解,但是很表面
6
1
删除
TCP四次挥手可以了解下
点赞
回复
雪飞鸿的头像
删除
.NET Core/Python/JavaScript
管道并不能保证命令执行顺序
4
3
删除
为什么?
点赞
回复
删除
管道是指客户端无需等待服务器返回数据便可继续发送请求,HTTP协议中也有管道
为什么?
点赞
回复
查看更多回复
L N W的头像
删除
请问只有使用了管道才会用缓存吗?
点赞
回复
xegk6un17n8tbpyggvche6uas的头像
删除
管道里面的操作应该不是事务的吧,
点赞
回复
海子同志的头像
删除
从文章上看起来管道是异步操作,可以说做了请求合并,但是应该和读写顺序无关,而不是像文章中提到合并write或者read请求
2
回复
battle_field的头像
删除
服务器应该也要提供支持的吧?至少协议层需要支持。服务器要区分每条消息,然后处理每条客户端请求后缓存结果,最后发送响应。没看过具体实现,不过我想服务器应该不会处理完每条消息都写到 sendbuffer 中吧,每次 write 也涉及用户态和内核态的切换。
1
回复
大大大桃子的头像
删除
Java开发
write发送网络包只能是串行的吗?如果不是串行的,那么并行和批量的差距有多大?
1
回复

查看全部 109 条回复