概念

为什么需要管道?

常见面试题:如何优化频繁命令往返造成的性能瓶颈?

Redis 是一种基于客户端/服务端模型以及请求/响应模型的 TCP 服务。一个请求会遵循:

  • 发送命令 → 命令排队 → 命令执行 → 返回结果
  • 客户端向服务端发送命令,并监听 Socket 返回,通常以阻塞模式等待服务端响应
  • 服务端处理命令,并将结果返回给客户端
  • 一次请求/响应时间称为 RTT(Round Trip Time,数据包往返于两端的时间)

如果同时需要执行大量命令,就要等待上一条命令应答后再执行,这中间不仅仅多了 RTT,而且还频繁调用系统 IO 发送网络请求,同时需要 Redis 服务端调用多次 read()write() 系统方法,速度就会降下来

管道可以将这 n 条命令一起发送到服务端

管道的本质

将要发往 Redis 服务端执行的命令在客户端缓存起来,然后将这些命令打包一次性发送到 Redis 服务端,从而将多个 RTT 降低到 1 个,大大降低了网络开销,提升了性能

  • 管道是客户端实现的技术,服务端稍微配合一下即可(读取批量命令执行,每条命令执行完后进行缓存,全部执行完后再一次性返回)
  • 一次性执行多个指令,减少 IO,缩减时间
  • 多个指令之间没有依赖关系,一个命令出问题,不影响其他命令
  • 注意:由于执行结果是批量返回的,所以管道中的命令不宜太多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存
  • 注意:管道中的命令适合彼此没有关联的命令,如果一个命令的执行依赖上次执行的结果,不要用管道

对比

管道 VS. 事务

  1. 命令在哪里缓存:
    • 事务中的命令在服务端缓存
    • 管道中的命令在客户端缓存
    • 但不管是事务还是管道,服务端都需要缓存单个命令的执行结果,等全部执行完后再返回给客户端
  2. 执行时机:
    • 事务一条一条地发通命令,通过 EXEC 命令开始先后顺序执行
    • 管道一次性将多条命令发到服务端,服务端读取后按照先后顺序执行
  3. 命令中出现编译时错误(语法错误),是否影响其他命令:
    • 事务里的命令如果有编译时错误,会导致事务被丢弃,里面的命令都不会执行
    • 管道里的命令如果有编译时错误,依然不影响其他的命令执行
  4. 命令执行过程
    • 执行事务时会阻塞其他命令的执行,保证原子性
    • 执行管道中的命令时不会,不保证原子性(虽然不保证原子性,但是因为 Redis 执行命令是单线程的,基本可以认为是原子性的)

管道 VS. 原生批量命令

  • 原生批量命令(如:msetmget)是原子性;管道不是
  • 原生批量命令一次只能执行一种命令;管道支持批量执行不同命令
  • 原生批量命令是原生实现的,性能很高;管道是一次性打包发送多条命令,没有特殊的性能优化

示例

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