Redis - Pipeline
Redis 的 pipeline(管道)功能在命令行中没有,但 redis 是支持 pipeline 的,而且在各个语言的 client 都有相应的实现。由于网络开销延迟,就算 redis server 端有很强的处理能力,也会由于收到的 client 消息少,而造成的吞吐量小。当 client 使用 pipeliing 发送命令时 redis server 必须将部分请求放到队列中(使用内存),执行完毕后一次性发送结果;如果发送的命令很多的话,建议对返回结果加标签,当然这样会增加使用的内存;
pipeline 在某些场景下非常有用,比如有多个command 需要被 “及时的” 提交,而且他们对相应结果没有互相依赖,对结果响应也无息立即获得,那么 pipeline 就可以充当这种 “批处理” 的工具;在一定程度上,可以较大的提升性能,性能提升的原因是 TCP 连接中减少了 “交互往返” 的时间。
Redis的消息交互
当我们使用客户端对 Redis 进行一次操作时,如下图所示,客户端将请求传送给服务器,服务器处理完毕后,再将响应回复给客户端。还要花费一个网络数据包来回对时间
如果连续执行多条指令,那就会花费多个网络数据包来回的时间
使用了pipeline执行N条命令
这便是管道操作的本质,服务器根本没有任何区别对待,还是收到一条消息,执行一条消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省 IO 时间。管道中指令越多,效果越好。
管道(pipeline) 可以一次性发送多条命令并在执行完成后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 pipeline 实现的原理就是队列,而队列的原理是先进先出,这样就保证数据的顺序性。pipeline 的默认的同步的个数为53个,也就是说 arges 中累加到53条数据时会把数据提交。过程如图:client 可以将三个命令放到一个 tcp 报问一起发送,server 则可以将三条命令的处理结果放到一个 tcp 报文返回。
需要注意到是用 pipeline 方式打包命令发送,redis 必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。
测试普通模式与Pipeline
/*
* 测试普通模式与 PipeLine 模式的效率:
* 测试方法:向 redis 中插入 10000 组数据
*/
public static void testPipeLineAndNormal(Jedis jedis)
throws InterruptedException {
Logger logger = Logger.getLogger("javasoft");
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
jedis.set(String.valueOf(i), String.valueOf(i));
}
long end = System.currentTimeMillis();
logger.info("the jedis total time is:" + (end - start));
Pipeline pipe = jedis.pipelined(); // 先创建一个 pipeline 的链接对象
long start_pipe = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pipe.set(String.valueOf(i), String.valueOf(i));
}
pipe.sync(); // 获取所有的 response
long end_pipe = System.currentTimeMillis();
logger.info("the pipe total time is:" + (end_pipe - start_pipe));
BlockingQueue<String> logQueue = new LinkedBlockingQueue<String>();
long begin = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
logQueue.put("i=" + i);
}
long stop = System.currentTimeMillis();
logger.info("the BlockingQueue total time is:" + (stop - begin));
}
从上述代码以及结果中可以明显的看到 PipeLine 在 “批量处理” 时的优势。
深入理解管道本质
管道的请求交互流程
上图就是一个完整的请求交互流程图
- 客户端进程调用 write 将消息写到操作系统内核为套接字分配的发送缓冲 send buffer。
- 客户端操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过【网络路由】送到服务器的网卡。
- 服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。
- 服务器进程调用read从接收缓冲中取出消息进行处理。
- 服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer。
- 服务器操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过【网络路由】送到客户端的网卡
- 客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。
- 客户端进程调用read从接收缓冲中取出消息返回给上层业务逻辑进行处理。
- 结束
我们开始以为 write 操作是要等到对方收到消息才会返回,但实际上不是这样的。write 操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间来,这个就是写操作 IO 操作的真正耗时。
我们开始以为 read 操作是从目标机器拉取数据,但实际上不是这样的。read 操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就需要等待数据到来,这个就是读操作 IO 操作的真正耗时。
所以对于value = redis.get(key)这样一个简单的请求来说,write操作几乎没有耗时,直接写到发送缓冲就返回,而read就会比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开销。
而对于管道来说,连续的write操作根本就没有耗时,之后第一个read操作会等待一个网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作直接就可以从缓冲拿到结果,瞬间就返回了。
小结
这就是管道的本质了,它并不是服务器的什么特性,而是客户端通过改变了读写的顺序带来的性能的巨大提升。