Redis客户端与服务器之间使用TCP协议进行通讯,而且很早就支持管道(pipelining)技术了。在某些高并发的场景下,网络开销成了Redis速度的瓶颈,因此须要使用管道技术来实现突破。java
在介绍管道以前,先来想一下单条命令的执行步骤:redis
按照这样的描述,每一个命令的执行时间 = 客户端发送时间+服务器处理和返回时间+一个网络来回的时间bash
其中一个网络来回的时间是不固定的,它的决定因素有不少,好比客户端到服务器要通过多少跳,网络是否拥堵等等。可是这个时间的量级也是最大的,也就是说一个命令的完成时间的长度很大程度上取决于网络开销。若是咱们的服务器每秒能够处理10万条请求,而网络开销是250毫秒,那么实际上每秒钟只能处理4个请求。最暴力的优化方法就是使客户端和服务器在一台物理机上,这样就能够将网络开销下降到1ms如下。可是实际的生产环境咱们并不会这样作。并且即便使用这种方法,当请求很是频繁时,这个时间和服务器处理时间比较仍然是很长的。服务器
为了解决这种问题,Redis在很早就支持了管道技术。也就是说客户端能够一次发送多条命令,不用逐条等待命令的返回值,而是到最后一块儿读取返回结果,这样只须要一次网络开销,速度就会获得明显的提高。管道技术其实已经很是成熟而且获得普遍应用了,例如POP3协议因为支持管道技术,从而显著提升了从服务器下载邮件的速度。网络
在Redis中,若是客户端使用管道发送了多条命令,那么服务器就会将多条命令放入一个队列中,这一操做会消耗必定的内存,因此管道中命令的数量并非越大越好(太大容易撑爆内存),而是应该有一个合理的值。并发
管道并不仅是用来网络开销延迟的一种方法,它其实是会提高Redis服务器每秒操做总数的。在解释缘由以前,须要更深刻的了解Redis命令处理过程。socket
一个完整的交互流程以下:高并发
write()
把消息写入到操做系统内核为Socket分配的send buffer中read()
读取消息而后进行处理write()
把返回结果写入到服务器端的send bufferread()
从recv buffer中读取消息并返回如今咱们把命令执行的时间进一步细分:优化
命令的执行时间 = 客户端调用write并写网卡时间+一次网络开销的时间+服务读网卡并调用read时间++服务器处理数据时间+服务端调用write并写网卡时间+客户端读网卡并调用read时间spa
这其中除了网络开销,花费时间最长的就是进行系统调用write()
和read()
了,这一过程须要操做系统由用户态切换到内核态,中间涉及到的上下文切换会浪费不少时间。
使用管道时,多个命令只会进行一次read()
和wrtie()
系统调用,所以使用管道会提高Redis服务器处理命令的速度,随着管道中命令的增多,服务器每秒处理请求的数量会线性增加,最后会趋近于不使用管道的10倍。
对于管道的大部分应用场景而言,使用Redis脚本(Redis2.6及之后的版本)会使服务器端有更好的表现。使用脚本最大的好处就是能够以最小的延迟读写数据。
有时咱们也须要在管道中使用EVAL和EVALSHA命令,这是彻底有可能的。所以Redis提供了SCRIPT LOAD命令来支持这种状况。
多说无益,仍是眼见为实。下面就来对比一下使用管道和不使用管道的速度差别。
public class JedisDemo {
private static int COMMAND_NUM = 1000;
private static String REDIS_HOST = "Redis服务器IP";
public static void main(String[] args) {
Jedis jedis = new Jedis(REDIS_HOST, 6379);
withoutPipeline(jedis);
withPipeline(jedis);
}
private static void withoutPipeline(Jedis jedis) {
Long start = System.currentTimeMillis();
for (int i = 0; i < COMMAND_NUM; i++) {
jedis.set("no_pipe_" + String.valueOf(i), String.valueOf(i), SetParams.setParams().ex(60));
}
long end = System.currentTimeMillis();
long cost = end - start;
System.out.println("withoutPipeline cost : " + cost + " ms");
}
private static void withPipeline(Jedis jedis) {
Pipeline pipe = jedis.pipelined();
long start_pipe = System.currentTimeMillis();
for (int i = 0; i < COMMAND_NUM; i++) {
pipe.set("pipe_" + String.valueOf(i), String.valueOf(i), SetParams.setParams().ex(60));
}
pipe.sync(); // 获取全部的response
long end_pipe = System.currentTimeMillis();
long cost_pipe = end_pipe - start_pipe;
System.out.println("withPipeline cost : " + cost_pipe + " ms");
}
}
复制代码
结果也符合咱们的预期:
withoutPipeline cost : 11791 ms
withPipeline cost : 55 ms
复制代码
read()
和write()
系统调用,以此来节约时间。前面咱们提到,为了解决网络开销带来的延迟问题,能够把客户端和服务器放到一台物理机上。可是有时用benchmark进行压测的时候发现这仍然很慢。
这时客户端和服务端实际是在一台物理机上的,全部的操做都在内存中进行,没有网络延迟,按理来讲这样的操做应该是很是快的。为何会出现上面的状况的呢?
实际上,这是由内核调度致使的。好比说,benchmark运行时,读取了服务器返回的结果,而后写了一个新的命令。这个命令就在回环接口的send buffer中了,若是要执行这个命令,内核须要唤醒Redis服务器进程。因此在某些状况下,本地接口也会出现相似于网络延迟的延迟。实际上是内核特别繁忙,一直没有调度到Redis服务器进程。
Redis源码
掘金小册:《Redis 深度历险:核心原理与应用实践》