The use of Redis pipes

The original address: https://blog.piaoruiqing.com/2019/06/24/redis

keywords

Redis Pipelining: The client can send multiple requests to the server without waiting for a response, which can be read in just one step.

Round Trip Time (RTT): indicates the Round Trip Time.

Why pipes

Redis is a TCP server using client-server model and Request/Response protocol. This means that requests are usually completed through the following steps:

  • The client sends a query to the server and usually reads the server response from the socket in blocking mode.
  • The server processes the command and sends the response back to the client.

The application connects to Redis over the network, which can be very fast (local loopback) or very slow. But regardless of the network latency, it takes time for a packet to travel from the client to the server, and then from the server back to the client for a reply (this time is called RTT). When a client needs to perform many requests in a row (for example, adding multiple elements to the same list or populating a database with multiple keys), it’s easy to see how this frequent operation can affect performance. Multiple operations are piped to the Redis server via a single IO, and the result of each instruction is retrieved at once to reduce overhead on the network.

The case of frequent operations without pipes is shown below:

After using the pipe, the picture is as follows:

How to use

Jedis

/** jedis pool */
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
private static final JedisPool POOL =
    new JedisPool(new JedisPoolConfig(), "test-redis-server".6379);
/** * test pipelining with Jedis */
@Test
public void testPipelining(a) {

    try (Jedis jedis = POOL.getResource()) {

        Pipeline pipelined = jedis.pipelined();	/ / (a)
        Response<String> response1 = pipelined.set("mykey1"."myvalue1");
        Response<String> response2 = pipelined.set("mykey2"."myvalue2");
        Response<String> response3 = pipelined.set("mykey3"."myvalue3");

        pipelined.sync();	/ / (2)

        LOGGER.info("cmd: SET mykey1 myvalue1, result: {}", response1.get());	/ / (3)
        LOGGER.info("cmd: SET mykey2 myvalue2, result: {}", response2.get());
        LOGGER.info("cmd: SET mykey3 myvalue3, result: {}", response3.get()); }}Copy the code
  • (a) :jedis.pipelined(): Get aPipelineFor batch execution of instructions.
  • (2) :pipelined.sync(): execute synchronously, by reading allResponseTo synchronize the pipe, which closes the pipe.
  • (3) :response1.get(): Obtains the execution result. Note: in executionpipelined.sync()Before,getYou can’t get the results.

Lettuce

 private final Logger LOGGER = LoggerFactory.getLogger(getClass());

/** redis client */
private static final RedisClient CLIENT
        = RedisClient.create("redis://@test-redis-server:6379/0");
/** * test pipelining with Lettuce */
@Test
public void testPipelining(a) throws ExecutionException, InterruptedException {

    try (StatefulRedisConnection<String, String> connection = CLIENT.connect()) {

        RedisAsyncCommands<String, String> async = connection.async();
        async.setAutoFlushCommands(false);
        RedisFuture<String> future1 = async.set("mykey1"."myvalue1");
        RedisFuture<String> future2 = async.set("mykey2"."myvalue2");
        RedisFuture<String> future3 = async.set("mykey3"."myvalue3");

        async.flushCommands();

        LOGGER.info("cmd: SET mykey1 myvalue1, result: {}", future1.get());
        LOGGER.info("cmd: SET mykey2 myvalue2, result: {}", future1.get());
        LOGGER.info("cmd: SET mykey3 myvalue3, result: {}", future1.get()); }}Copy the code

RedisTemplate

private final Logger LOGGER = LoggerFactory.getLogger(getClass());

@Resource
private StringRedisTemplate stringRedisTemplate;

/** * test pipelining with RedisTemplate */
@Test
public void testPipelining(a) {

    List<Object> objects 
        = stringRedisTemplate.executePipelined((RedisCallback<Object>)connection -> {

        connection.set("mykey1".getBytes(), "myvalue1".getBytes());
        connection.set("mykey2".getBytes(), "myvalue2".getBytes());
        connection.set("mykey3".getBytes(), "myvalue3".getBytes());
        return null;	/ / (a)
    });

    LOGGER.info("cmd: SET mykey myvalue, result: {}", objects);
}
Copy the code
  • (a): this must be returnednull

Simple comparison test

Redis servers run on raspberry PI under the same router.

/** * pipeline vs direct */
@Test
public void compared(a) {

    try (Jedis jedis = POOL.getResource()) {   // warm up
        jedis.set("mykey"."myvalue");
    }

    try (Jedis jedis = POOL.getResource()) {
        long start = System.nanoTime();
        Pipeline pipelined = jedis.pipelined();
        for (int index = 0; index < 500; index++) {
            pipelined.set("mykey" + index, "myvalue" + index);
        }
        pipelined.sync();
        long end = System.nanoTime();
        LOGGER.info("pipeline cost: {} ns", end - start);
    }

    try (Jedis jedis = POOL.getResource()) {
        long start = System.nanoTime();
        for (int index = 0; index < 500; index++) {
            jedis.set("mykey" + index, "myvalue" + index);
        }
        long end = System.nanoTime();
        LOGGER.info("direct cost: {} ns", end - start); }}Copy the code

Execute 500 sets using Jedis as follows:

22:16:00.523 [main] info-direct pipeline cost: 73681257 ns // pipeline 22:16:03.040 [main] info-direct pipeline cost: 73681257 ns // pipeline 22:16:03.040 [main] info-direct pipeline cost: 73681257 ns // pipeline 22:16:03.040 [main] info-direct pipeline cost: 2511915103 ns // Run this command directlyCopy the code

The sum of the 500 set execution times is no longer on the same order of magnitude as the time taken for a single pipe execution.

extension

Excerpt from redis official documentation

Not only does using pipes reduce RTT to reduce latency costs, but using pipes can actually greatly increase the total number of operations per second that can be performed on the Redis server. This is because, although it is easy to operate a single command without pipes, the actual cost of this frequent I/O operation is huge, involving calls to system reads and writes, which means from the user domain to the kernel domain. Context switching takes a huge toll on speed.

When using pipes, it is common to use a single read() system call to read many commands and pass multiple replies through a single write() system call. As a result, the total number of queries executed per second initially increases linearly with longer pipes and eventually reaches 10 times that without pipes, as shown in the figure below:

series

  1. Introduction to Redis
  2. Redis uses advanced levels
  3. Redis distributed lock

reference

  • redis.io
[Copyright Notice]


This article was published on
Park Seo-kyung’s blog, allow non-commercial reprint, but reprint must retain the original author
PiaoRuiQingAnd links:
blog.piaoruiqing.comFor negotiation or cooperation on authorization, please contact:
[email protected].