"After all, the engineers only needed to refuse to fix anything, and modern industry would grind to a halt." -Michael Lewis

Enable Massive Growth

Working with Lists in Redis using Lettuce and Webflux

Apr 2021

As of this writing, there are a solid twenty or so commands you can execute against redis for the list data type. This article will be walking through some of the more common operations you are likely to need when interacting with redis and lists using lettuce, and the source code can be found on Github.

Building off of a previous post where we set up a redis test container for testing lettuce, we can take that setup and teardown code and make it a base abstract class for reuse:

@Testcontainers
public abstract class BaseSetupAndTeardownRedis {

    @Container
    public static GenericContainer genericContainer = new GenericContainer(
            DockerImageName.parse("redis:5.0.3-alpine")
    ).withExposedPorts(6379);

    protected RedisClient redisClient;

    @BeforeEach
    public void setupRedisClient() {
        redisClient = RedisClient.create("redis://" + genericContainer.getHost() + ":" + genericContainer.getMappedPort(6379));
    }

    @AfterEach
    public void removeAllDataFromRedis() {
        redisClient.connect().reactive().flushall().block();
    }
}

This just starts our redis container, configures our redis client to communicate with it by default, then deletes all the data out of redis after each test case has run. With this in place, we can start writing some test cases demonstrating how we can interact with lists in redis.

Push and Pop

One of the more common things you're likely to do against redis lists is just adding and removing elements from the "left" or "right". We'll demonstrate how to remove from the left here:

    @Test
    public void addAndRemoveFromTheLeft() {
        RedisReactiveCommands<String, String> redisReactiveCommands = redisClient.connect().reactive();

        StepVerifier.create(redisReactiveCommands.lpush("list-key", "fourth-element", "third-element"))
                .expectNextMatches(sizeOfList -> 2L == sizeOfList)
                .verifyComplete();

        StepVerifier.create(redisReactiveCommands.lpush("list-key","second-element", "first-element"))
                // pushes to the left of the same list
                .expectNextMatches(sizeOfList -> 4L == sizeOfList)
                .verifyComplete();

        StepVerifier.create(redisReactiveCommands.lpop("list-key"))
                .expectNextMatches(poppedElement -> "first-element".equals(poppedElement))
                .verifyComplete();
    }

We insert elements four, three, two, then one from left to right. This leads to a list that looks like first-element -> second-element -> third-element -> fourth-element. we then pop an element off the "left" of the list to grab the first element, then verify that is indeed what we're getting.

Blocking Get

This one is more interesting. The blpop operation will block until an element becomes available [for a specified number of seconds]. If one doesn't become available in time, it will release itself. Here's an example where we execute a blpop and we then push an element into the list about half a second later, asserting that the amount of time that took was at least half a second [ish. I made it 400 ms mostly out of paranoia]:

    @Test
    public void blockingGet() {
        RedisReactiveCommands<String, String> redisReactiveCommands1 = redisClient.connect().reactive();
        RedisReactiveCommands<String, String> redisReactiveCommands2 = redisClient.connect().reactive();

        long startingTime = Instant.now().toEpochMilli();
        StepVerifier.create(Mono.zip(
                    redisReactiveCommands1.blpop(1, "list-key").switchIfEmpty(Mono.just(KeyValue.empty("list-key"))),
                    Mono.delay(Duration.ofMillis(500)).then(redisReactiveCommands2.lpush("list-key", "an-element"))
                ).map(tuple -> tuple.getT1().getValue())
            )
            .expectNextMatches(value -> "an-element".equals(value))
            .verifyComplete();
        long endingTime = Instant.now().toEpochMilli();

        assertTrue(endingTime - startingTime > 400);
    }

Range of Elements

If you want to just look at any given range of elements, you can do that with lrange. This command will iterate from left to right and pull out elements as it finds them between the indices that you specify:

    @Test
    public void getRange() {
        RedisReactiveCommands<String, String> redisReactiveCommands = redisClient.connect().reactive();

        StepVerifier.create(redisReactiveCommands.lpush("list-key", "third-element", "second-element", "first-element"))
                .expectNextMatches(sizeOfList -> 3L == sizeOfList)
                .verifyComplete();

        StepVerifier.create(redisReactiveCommands.lrange("list-key", 0, 1))
                .expectNextMatches(first -> "first-element".equals(first))
                .expectNextMatches(second -> "second-element".equals(second))
                .verifyComplete();
    }

It's important to note that for very large lists, this operation could take more time than you would like, because redis lists are implemented as linked lists. Therefore getting elements towards the middle of the list will be a O(N) operation.

Nick Fisher is a software engineer in the Pacific Northwest. He focuses on building highly scalable and maintainable backend systems.