Redis 的管道技术实现

227 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第22天,点击查看活动详情

使用管道技术可以将客户端需要连续执行多次请求的操作通过一次 IO 发送给 Redis 服务器,然后一次性获取每一条指令的结果,以减少网络上的开销。接下来我们一起来学习如此神奇的管道技术吧~😊

管道(Pipelining)介绍

我们知道 Redis 采用的是 C/S 架构,客户端和服务端通过 TCP 协议进行连接通信,因此无论发出请求还是接收响应,都必须经过网络传输。这意味通常情况下一个请求会遵循以下步骤:

  • 客户端向服务器发送一个查询请求,并监听 Socket 返回,通常是以阻塞模式,等待服务器响应。
  • 服务端处理命令,并将结果返回给客户端。

应用程序与 Redis 经过网络进行连接,可能会很快(比如:本地回环 - loopback),也可能会很慢(比如:建立一个多次跳转的网络连接)。无论网络如何延时,数据包总是从客户端到达服务器,并从服务器返回数据恢复客户端,这都是需要时间的,这期间的时间被称为 RTT(Round Trip Time - 往返时间)。

当客户端需要连续执行多次请求时(比如:将多个元素添加到同一个 list,或使用多个 keys 填充数据库),很容易发现这种频繁操作很影响性能。

那有没有什么好的解决办法呢?

当然有,Redis 中的管道(Pipelining)技术就可以帮助我们解决这个问题。

管道技术 即在旧的请求还未被响应的情况下,一次请求/响应服务器也能够处理新的请求。可以将多个命令发送到服务器,不用等待回复,而后一次性获取每一条指令的结果,这就是管道技术。

下面我们通过以下两种场景来了解管道技术的实现:

  • 频繁操作但未使用管道,如下图所示:

图片描述

  • 频繁操作但使用管道,如下图所示:

图片描述

上面两个图就是使用管道和未使用管道,可以很明显看出,使用管道技术大大减少了 RTT 的开销。

🔍 强调:使用 Redis 管道时,需要注意以下几点:

  • 由于 Redis 的管道要求服务器一次性的将请求返回,因此 Redis 服务端只能将靠前命令处理的结果暂时缓存起来。如果管道一次响应的数据量过多(大规模查询之类的),可能会对 Redis 服务器的内存造成较大的压力。因此,管道批量处理的命令数量并不是越多越好,需要结合实际需求,合理的决定一次管道批处理命令的数量。
  • Redis 的管道在客户端通常会设置一个命令缓冲区来存储即将被批量发送的命令,当缓冲区被填满时,才会一次性的将缓冲区的命令发送。这里需要注意的一点是:当业务上的同一批命令使用管道进行请求时,如果最后剩余的命令无法填满缓冲区,如果不使用相应的 flush 操作,这些命令将不会被发送出去,而是保留在命令缓冲区等待新的命令来填满缓冲区。

管道使用

Jedis 提供的管道操作 API

  • 在 RedisPipelinedTest 测试类添加一个 testJedisPipelining 方法,使用管道技术新增 string 类型的数据,代码如下所示 👇:

    @Test
    public void testJedisPipelining(){
      // 创建 Jedis 对象
      Jedis jedis = new Jedis("127.0.0.1",6379);
    
      // 获取一个 Pipeline 对象,用以批量执行指令。
      Pipeline pipelined = jedis.pipelined();
    
      Response<String> response1 = pipelined.set("name1", "zhangsan");
      Response<String> response2 = pipelined.set("name2", "lisi");
      Response<String> response3 = pipelined.set("name3", "zhangwuji");
    
      // 同步执行, 经过读取所有 Response 来同步管道, 这个操做会关闭管道。
      pipelined.sync();
    
      // 获取执行结果(在执行 pipelined.sync() 以前, get 是没法获取到结果的)。
      System.out.println("cmd: set name1 zhangsan, result: {"+ response1.get()+"}");
      System.out.println("cmd: set name2 lisi, result: {"+ response2.get()+"}");
    
      // 关闭连接,释放资源
      jedis.close();
    }
    

➡️ RedisTemplate 提供的管道操作 API

  • 在 RedisPipelinedTest 测试类添加一个 testRedisTemplatePipelining 方法,使用管道技术新增 list 类型的数据,代码如下所示 👇:

    @Test
    public void testRedisTemplatePipelining(){
    
      List<Object> objects = redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
        conn.set("name3".getBytes(), "zhangsan".getBytes());
        conn.set("name4".getBytes(), "zhangsanfeng".getBytes());
        conn.set("name5".getBytes(), "zhangwuji".getBytes());
        return null;
      });
    
      // 获取执行结果
      System.out.println("cmd: set keys values, result: {"+ objects+"}");
    }
    

    ⚠️ 注意:这里的 return null; 必须添加,不能省略。

    执行之后的结果,如下图所示:

    图片描述

➡️ 简单对比测试(以 RedisTemplate 操作为例)

  • 在 RedisPipelinedTest 测试类添加一个 testRedisTemplatePipeliningAndDirectCompare 方法,使用管道技术新增 list 类型的数据,代码如下所示 👇:

    @Test
    public void testRedisTemplatePipeliningAndDirectCompare(){
      // 直接运行
      long d_start = System.nanoTime();
      for (int index = 0; index < 500; index++) {
        redisTemplate.opsForValue().set("key"+index, index+"");
      }
      long d_end = System.nanoTime();
      System.out.println("直接执行耗费的时间: {"+ (d_end-d_start) +" ns}");
    
      // 通过管道执行
      long p_start = System.nanoTime();
      List<Object> objects = redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
        for (int index = 0; index < 500; index++) {
          conn.set(("key"+index).getBytes(), (index+"").getBytes());
        }
        return null;
      });
      long p_end = System.nanoTime();
      System.out.println("管道执行耗费的时间: {"+ (p_end-p_start) +" ns}");
    }
    

    执行之后的结果,如下图所示:

    图片描述

    从上图可以看出,直接使用 redisTemplate 调用 API 执行 500 条数据和用管道执行一次耗费的时间相差很大。