本文转自:分布式缓存Redis之Pipeline(管道)
本学习教程所有示例代码见GitHub:https://github.com/selfconzrr/Redis_Learning
Redis的pipeline(管道)功能在命令行中没有,但redis是支持pipeline的,而且在各个语言版的client中都有相应的实现。 由于网络开销延迟,就算redis server端有很强的处理能力,也会由于收到的client消息少,而造成吞吐量小。当client 使用pipelining 发送命令时,redis server必须将部分请求放到队列中(使用内存),执行完毕后一次性发送结果;如果发送的命令很多的话,建议对返回的结果加标签,当然这也会增加使用的内存;
Pipeline在某些场景下非常有用,比如有多个command需要被“及时的”提交,而且他们对相应结果没有互相依赖,对结果响应也无需立即获得,那么pipeline就可以充当这种“批处理”的工具;而且在一定程度上,可以较大的提升性能,性能提升的原因主要是TCP连接中减少了“交互往返”的时间。
不过在编码时请注意,pipeline期间将“独占”链接,此期间将不能进行非“管道”类型的其他操作,直到pipeline关闭;如果你的pipeline的指令集很庞大,为了不干扰链接中的其他操作,你可以为pipeline操作新建Client链接,让pipeline和其他正常操作分离在2个client中。不过pipeline事实上所能容忍的操作个数,和socket-output缓冲区大小/返回结果的数据尺寸都有很大的关系;同时也意味着每个redis-server同时所能支撑的pipeline链接的个数,也是有限的,这将受限于server的物理内存或网络接口的缓冲能力。
Redis使用的是客户端-服务器(CS)模型和请求/响应协议的TCP服务器。这意味着通常情况下一个请求会遵循以下步骤:
其执行过程如下图所示:
由于通信会有网络延迟,假如client和server之间的包传输时间需要0.125秒。那么上面的三个命令6个报文至少需要0.75秒才能完成。这样即使redis每秒能处理100个命令,而我们的client也只能一秒钟发出四个命令。这显然没有充分利用 redis的处理能力。
而管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间,而且Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。 Pipeline 的默认的同步的个数为53个,也就是说arges中累加到53条数据时会把数据提交。其过程如下图所示:client可以将三个命令放到一个tcp报文一起发送,server则可以将三条命令的处理结果放到一个tcp报文返回。
需要注意到是用 pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。
测试环境:
Windows:Eclipse + jedis2.9.0 + jdk 1.7
Ubuntu:部署在虚拟机上的服务器 Redis 3.0.7
@Test
public void testPipeLineAndNormal() throws InterruptedException {
final int loopNum = 10000;
Jedis jedis = RedisUtil.getJedis();
jedis.select(2);
jedis.flushDB();
long start = System.currentTimeMillis();
for (int i = 0; i < loopNum; i++) {
jedis.set(String.valueOf(i), String.valueOf(i));
}
long end = System.currentTimeMillis();
System.out.println("the jedis total time is:" + (end - start));
Pipeline pipe = jedis.pipelined();
long start_pipe = System.currentTimeMillis();
for (int i = 0; i < loopNum; i++) {
pipe.set("1" + String.valueOf(i), String.valueOf(i));
}
pipe.sync(); // 调用sync会关闭管道,所以在调用sync之后就不可以在使用管道了
long end_pipe = System.currentTimeMillis();
System.out.println("the pipe total time is:" + (end_pipe - start_pipe));
BlockingQueue logQueue = new LinkedBlockingQueue();
long begin = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
logQueue.put("i=" + i);
}
long stop = System.currentTimeMillis();
System.out.println("the BlockingQueue total time is:" + (stop - begin));
}
执行结果:
the jedis total time is:1777
the pipe total time is:57
the BlockingQueue total time is:7
从上述代码以及结果中可以明显的看到PipeLine在“批量处理”时的优势。
另外,使用管道时,可以使用 Response 对象来获取命令的执行结果。如下面的示例(例子来自:redis基础简介(六)- jedis使用管道(pipeline)对redis进行读写(使用hmset、hgetall测试))。
@Test
public void testPipeLine2() {
Jedis redis = RedisUtil.getJedis();
redis.select(8);
redis.flushDB();
Map data = new HashMap();
// hmset
final int loopNum = 10000;
long start = System.currentTimeMillis();
for(int i = 0; i keys = redis.keys("*");
// 直接使用Jedis hgetall
start = System.currentTimeMillis();
Map> result = new HashMap>();
for (String key : keys) {
result.put(key, redis.hgetAll(key));
}
end = System.currentTimeMillis();
System.out.println("result size:[" + result.size() + "] ..");
System.out.println("hgetAll without pipeline used [" + (end-start) + "] miliseconds ..");
// 使用pipeline hgetall
Map>> responses =
new HashMap>>(
keys.size());
result.clear();
start = System.currentTimeMillis();
for (String key : keys) {
responses.put(key, p.hgetAll(key));
}
p.sync();
for (String k : responses.keySet()) {
result.put(k, responses.get(k).get());
}
end = System.currentTimeMillis();
System.out.println("result size:[" + result.size() + "] ..");
System.out.println("hgetAll with pipeline used [" + (end-start) + "] miliseconds ..");
redis.disconnect();
}
有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进redis了,那这种场景就不适合。
还有的系统,可能是批量的将数据写入redis,允许一定比例的写入失败,那么这种场景就可以使用了,比如10000条一下进入redis,可能失败了2条无所谓,后期有补偿机制就行了,比如短信群发这种场景,如果一下群发10000条,按照第一种模式去实现,那这个请求过来,要很久才能给客户端响应,这个延迟就太长了,如果客户端请求设置了超时时间5秒,那肯定就抛出异常了,而且本身群发短信要求实时性也没那么高,这时候用pipeline最好了。
管道只适用于无因果关联的多命令操作,否则就需要借助 Lua 脚本实现批量操作;
大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。
应用程序有时可能在 pipeline 中发送 EVAL 或 EVALSHA 命令。Redis 通过 SCRIPT LOAD 命令(保证 EVALSHA 成功被调用)明确支持这种情况。
关于Pipeline的源码分析 请看后续文章分析。