概念
为什么需要管道?
常见面试题:如何优化频繁命令往返造成的性能瓶颈?
Redis 是一种基于客户端/服务端模型以及请求/响应模型的 TCP 服务。一个请求会遵循:
- 发送命令 → 命令排队 → 命令执行 → 返回结果
- 客户端向服务端发送命令,并监听 Socket 返回,通常以阻塞模式等待服务端响应
- 服务端处理命令,并将结果返回给客户端
- 一次请求/响应时间称为 RTT(Round Trip Time,数据包往返于两端的时间)
如果同时需要执行大量命令,就要等待上一条命令应答后再执行,这中间不仅仅多了 RTT,而且还频繁调用系统 IO 发送网络请求,同时需要 Redis 服务端调用多次 read()
和 write()
系统方法,速度就会降下来
管道可以将这 n 条命令一起发送到服务端
管道的本质
将要发往 Redis 服务端执行的命令在客户端缓存起来,然后将这些命令打包一次性发送到 Redis 服务端,从而将多个 RTT 降低到 1 个,大大降低了网络开销,提升了性能
- 管道是客户端实现的技术,服务端稍微配合一下即可(读取批量命令执行,每条命令执行完后进行缓存,全部执行完后再一次性返回)
- 一次性执行多个指令,减少 IO,缩减时间
- 多个指令之间没有依赖关系,一个命令出问题,不影响其他命令
- 注意:由于执行结果是批量返回的,所以管道中的命令不宜太多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存
- 注意:管道中的命令适合彼此没有关联的命令,如果一个命令的执行依赖上次执行的结果,不要用管道
对比
管道 VS. 事务
- 命令在哪里缓存:
- 事务中的命令在服务端缓存
- 管道中的命令在客户端缓存
- 但不管是事务还是管道,服务端都需要缓存单个命令的执行结果,等全部执行完后再返回给客户端
- 执行时机:
- 事务一条一条地发通命令,通过
EXEC
命令开始先后顺序执行 - 管道一次性将多条命令发到服务端,服务端读取后按照先后顺序执行
- 事务一条一条地发通命令,通过
- 命令中出现编译时错误(语法错误),是否影响其他命令:
- 事务里的命令如果有编译时错误,会导致事务被丢弃,里面的命令都不会执行
- 管道里的命令如果有编译时错误,依然不影响其他的命令执行
- 命令执行过程
- 执行事务时会阻塞其他命令的执行,保证原子性
- 执行管道中的命令时不会,不保证原子性(虽然不保证原子性,但是因为 Redis 执行命令是单线程的,基本可以认为是原子性的)
管道 VS. 原生批量命令
- 原生批量命令(如:
mset
、mget
)是原子性;管道不是 - 原生批量命令一次只能执行一种命令;管道支持批量执行不同命令
- 原生批量命令是原生实现的,性能很高;管道是一次性打包发送多条命令,没有特殊的性能优化
示例
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