lettuce Documentation

Advanced Java Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs.

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy lettuce on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name lettuce -p 8474:8474 --restart always ghcr.io/shopify/toxiproxy:2.8.0
Configured Services & Ports
โš™๏ธ toxiproxy
image: ghcr.io/shopify/toxiproxy:2.8.0
๐Ÿ”Œ Ports: 8474:8474, 9479:9479, 9480:9480, 9481:9481
โš™๏ธ redis-standalone-multi
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6487:6487, 6488:6488, 6489:6489, 6490:6490, 6491:6491
โš™๏ธ redis-standalone-0
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6478:6478, 6444:6444
โš™๏ธ redis-standalone-1
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6479:6479, 6486:6486, 6443:6443
โš™๏ธ redis-standalone-2
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6480:6480
โš™๏ธ redis-standalone-3
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6481:6481
โš™๏ธ redis-standalone-4
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6484:6484, 26381:26381
โš™๏ธ redis-standalone-5-client-cert
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 6485:6485, 6445:6445
โš™๏ธ redis-standalone-sentinel-controlled
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 26380:26380, 26822:26822, 26379:26379, 6482:6482, 6483:6483, 8443:8443, 8444:8444
โš™๏ธ standalone-stack
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_STACK_VERSION:-${REDIS_VERSION:-8.8.0}}}
๐Ÿ”Œ Ports: 16379:6379
โš™๏ธ clustered-stack
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_STACK_VERSION:-${REDIS_VERSION:-8.8.0}}}
๐Ÿ”Œ Ports: 36379-36381:36379-36381
โš™๏ธ ssl-test-cluster
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 7479:7479, 7480:7480, 7481:7481, 7443:7443, 7444:7444, 7445:7445
โš™๏ธ test-cluster
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
๐Ÿ”Œ Ports: 7379:7379, 7380:7380, 7381:7381, 7382:7382, 7383:7383, 7384:7384, 7385:7385, 7582:7582
โš™๏ธ cleanup
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}
docker-compose.yml
yaml (docker-compose.yml)
x-client-libs-image: &client-libs-image
  image: "${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_VERSION:-8.8.0}}"
x-client-libs-stack-image: &client-libs-stack-image
  image: "${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test}:${CLIENT_LIBS_TEST_IMAGE_TAG:-${REDIS_STACK_VERSION:-${REDIS_VERSION:-8.8.0}}}"

services:
  # Test infrastructure used for simulating network issues to test multi-db client failover
  # includes standalone redis instances with toxiproxy in front of them allowing enabling/disabling
  # access to the redis instances
  toxiproxy:
    image: ghcr.io/shopify/toxiproxy:2.8.0
    ports:
      - "8474:8474"   # Admin API
      - "9479:9479"   # Proxy port for redis instance 1
      - "9480:9480"   # Proxy port for redis instance 2
      - "9481:9481"   # Proxy port for redis instance 3
    networks:
      - redis-network

  redis-standalone-multi:
    <<: *client-libs-image
    container_name: redis-failover
    environment:
      - PORT=6487
      - NODES=5
    volumes:
      - ${REDIS_ENV_WORK_DIR}/redis-standalone-multi/work:/redis/work:rw
    ports:
      - "6487:6487"
      - "6488:6488"
      - "6489:6489"
      - "6490:6490"
      - "6491:6491"
    networks:
      - redis-network

  # Standalone Redis Servers
  redis-standalone-0:
    <<: *client-libs-image
    container_name: redis-standalone-0
    environment:
      - TLS_ENABLED=yes
    volumes:
      - ./redis-standalone-0/config:/redis/config:r
      - ${REDIS_ENV_WORK_DIR}/redis-standalone-0/work:/redis/work:rw
    ports:
      - "6478:6478"
      - "6444:6444" # TLS Port
    networks:
      - redis-network

  redis-standalone-1:
    <<: *client-libs-image
    container_name: redis-standalone-1
    environment:
      - TLS_ENABLED=yes
    volumes:
      - ./redis-standalone-1/config:/redis/config:r
      - ${REDIS_ENV_WORK_DIR}/redis-standalone-1/work:/redis/work:rw
    ports:
      - "6479:6479"
      - "6486:6486"
      - "6443:6443" # TLS Port
    networks:
      - redis-network

  redis-standalone-2:
    <<: *client-libs-image
    container_name: redis-standalone-2
    volumes:
      - ./redis-standalone-2/config:/redis/config:r
    ports:
      - "6480:6480"
    networks:
      - redis-network

  redis-standalone-3:
    <<: *client-libs-image
    container_name: redis-standalone-3
    volumes:
      - ./redis-standalone-3/config:/redis/config:r
    ports:
      - "6481:6481"
    networks:
      - redis-network

  redis-standalone-4:
    <<: *client-libs-image
    container_name: redis-standalone-4
    volumes:
      - ./redis-standalone-4/config:/redis/config:r

    environment:
      - REDIS_CLUSTER=no
    ports:
      - "6484:6484"
      - "26381:26381"
    networks:
      - redis-network

  redis-standalone-5-client-cert:
    <<: *client-libs-image
    container_name: redis-standalone-5-client-cert
    environment:
      - TLS_ENABLED=yes
    volumes:
      - ./redis-standalone-5-client-cert/config:/redis/config:r
      - ${REDIS_ENV_WORK_DIR}/redis-standalone-5-client-cert/work:/redis/work:rw
    ports:
      - "6485:6485"
      - "6445:6445" # TLS Port
    networks:
      - redis-network

  redis-standalone-sentinel-controlled:
    <<: *client-libs-image
    container_name: redis-standalone-sentinel-controlled
    environment:
      - REDIS_CLUSTER=no
      - TLS_ENABLED=yes
    volumes:
      - ./redis-standalone-sentinel-controlled/config:/redis/config:r
      - ${REDIS_ENV_WORK_DIR}/redis-standalone-sentinel-controlled/work:/redis/work:rw
      - ${REDIS_ENV_WORK_DIR}:/work
    ports:
      - "26380:26380"
      - "26822:26822" # sentinel tls port
      - "26379:26379"
      - "6482:6482"
      - "6483:6483"
      - "8443:8443" # TLS Port
      - "8444:8444" # TLS Port
    networks:
      - redis-network

  standalone-stack:
    <<: *client-libs-stack-image
    environment:
      - REDIS_CLUSTER=no
      - PORT=6379
    ports:
      - "16379:6379"

  clustered-stack:
    <<: *client-libs-stack-image
    environment:
      - REDIS_CLUSTER=yes
      - PORT=36379
    ports:
      - "36379-36381:36379-36381"

  # TLS Cluster with native Redis TLS and client certificate authentication (mTLS)
  ssl-test-cluster:
    <<: *client-libs-image
    container_name: ssl-test-cluster
    environment:
      - REDIS_CLUSTER=yes
      - TLS_ENABLED=yes
    volumes:
      - ./ssl-test-cluster/config:/redis/config:r
      - ${REDIS_ENV_WORK_DIR}/ssl-test-cluster/work:/redis/work:rw
    ports:
      - "7479:7479"
      - "7480:7480"
      - "7481:7481"
      - "7443:7443"   # TLS port node 1
      - "7444:7444"   # TLS port node 2
      - "7445:7445"   # TLS port node 3
    networks:
      - redis-network

  # Non-SSL Cluster
  test-cluster:
    <<: *client-libs-image
    container_name: test-cluster
    environment:
      - REDIS_CLUSTER=yes
    volumes:
      - ./test-cluster/config:/redis/config:r
    ports:
      - "7379:7379"
      - "7380:7380"
      - "7381:7381"
      - "7382:7382"
      - "7383:7383"
      - "7384:7384"
      - "7385:7385"
      - "7582:7582"
    networks:
      - redis-network

  # Cleanup workdir using container uid/gid to avoid host permission issues.
  cleanup:
    <<: *client-libs-image
    volumes:
      - ${REDIS_ENV_WORK_DIR}:/work:rw
    entrypoint: find /work -mindepth 1 -delete
    profiles:
      - cleanup

networks:
  redis-network:
    driver: bridge

1. Project Overview & Quickstart (redis/lettuce)

File: README.md

Lettuce - Advanced Java Redis client




Maven Central





Lettuce is a scalable thread-safe Redis client for synchronous,
asynchronous and reactive usage. Multiple threads may share one connection if they avoid blocking and transactional
operations such as BLPOP and MULTI/EXEC.
Lettuce is built with netty.
Supports advanced Redis features such as Sentinel, Cluster, Pipelining, Auto-Reconnect and Redis data models.

This version of Lettuce has been tested against the latest Redis source-build.

See the reference documentation and API Reference for more details.

How do I Redis?

Learn for free at Redis University

Try the Redis Cloud

Dive in developer tutorials

Join the Redis community

Work at Redis

Documentation

Binaries/Download

Binaries and dependency information for Maven, Ivy, Gradle and others can be found at http://search.maven.org.

Releases of lettuce are available in the Maven Central repository. Take also a look at the Releases.

Example for Maven:

xml
<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>x.y.z</version>
</dependency>

If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version.

xml
<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>x.y.z.BUILD-SNAPSHOT</version>
</dependency>

<repositories>
  <repository>
    sonatype-snapshots
    <name>Sonatype Snapshot Repository</name>
    https://oss.sonatype.org/content/repositories/snapshots/
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
  </repository>
</repositories>

Basic Usage

java
RedisClient client = RedisClient.create("redis://localhost");
StatefulRedisConnection<String, String> connection = client.connect();
RedisStringCommands sync = connection.sync();
String value = sync.get("key");

Each Redis command is implemented by one or more methods with names identical
to the lowercase Redis command name. Complex commands with multiple modifiers
that change the result type include the CamelCased modifier as part of the
command name, e.g. zrangebyscore and zrangebyscoreWithScores.

See Basic usage for further details.

Asynchronous API

java
StatefulRedisConnection<String, String> connection = client.connect();
RedisStringAsyncCommands<String, String> async = connection.async();
RedisFuture<String> set = async.set("key", "value");
RedisFuture<String> get = async.get("key");

LettuceFutures.awaitAll(set, get) == true

set.get() == "OK"
get.get() == "value"

See Asynchronous API for further details.

Reactive API

java
StatefulRedisConnection<String, String> connection = client.connect();
RedisStringReactiveCommands<String, String> reactive = connection.reactive();
Mono<String> set = reactive.set("key", "value");
Mono<String> get = reactive.get("key");

set.subscribe();

get.block() == "value"

See Reactive API for further details.

Pub/Sub

java
RedisPubSubCommands<String, String> connection = client.connectPubSub().sync();
connection.getStatefulConnection().addListener(new RedisPubSubListener<String, String>() { ... })
connection.subscribe("channel");

Building

Lettuce is built with Apache Maven. The tests require multiple running Redis instances for different test cases which
are configured using a Makefile. Tests run by default against Redis latest.

To build:

text
$ git clone https://github.com/redis/lettuce.git
$ cd lettuce/
$ make start
  • Run the build: make test
  • Start Redis (manually): make start
  • Stop Redis (manually): make stop
  • Clean up: make clean

Bugs and Feedback

For bugs, questions and discussions please use the GitHub Issues.

License

Contributing

Github is for social coding: if you want to write code, I encourage contributions through pull requests from forks of this repository.
Create Github tickets for bugs and new features and comment on the ones that you are interested in and take a look into CONTRIBUTING.md


File: docs/user-guide/async-api.md

Asynchronous API

This guide will give you an impression how and when to use the
asynchronous API provided by Lettuce 4.x.

Motivation

Asynchronous methodologies allow you to utilize better system resources,
instead of wasting threads waiting for network or disk I/O. Threads can
be fully utilized to perform other work instead. Lettuce facilitates
asynchronicity from building the client on top of
netty that is a multithreaded, event-driven I/O
framework. All communication is handled asynchronously. Once the
foundation is able to processes commands concurrently, it is convenient
to take advantage from the asynchronicity. It is way harder to turn a
blocking and synchronous working software into a concurrently processing
system.

Understanding Asynchronicity

Asynchronicity permits other processing to continue before the
transmission has finished and the response of the transmission is
processed. This means, in the context of Lettuce and especially Redis,
that multiple commands can be issued serially without the need of
waiting to finish the preceding command. This mode of operation is also
known as Pipelining. The following
example should give you an impression of the mode of operation:

  • Given client A and client B

  • Client A triggers command SET A=B

  • Client B triggers at the same time of Client A command SET C=D

  • Redis receives command from Client A

  • Redis receives command from Client B

  • Redis processes SET A=B and responds OK to Client A

  • Client A receives the response and stores the response in the
    response handle

  • Redis processes SET C=D and responds OK to Client B

  • Client B receives the response and stores the response in the
    response handle

Both clients from the example above can be either two threads or
connections within an application or two physically separated clients.

Clients can operate concurrently to each other by either being separate
processes, threads, event-loops, actors, fibers, etc. Redis processes
incoming commands serially and operates mostly single-threaded. This
means, commands are processed in the order they are received with some
characteristic that weโ€™ll cover later.

Letโ€™s take the simplified example and enhance it by some program flow
details:

  • Given client A

  • Client A triggers command SET A=B

  • Client A uses the asynchronous API and can perform other processing

  • Redis receives command from Client A

  • Redis processes SET A=B and responds OK to Client A

  • Client A receives the response and stores the response in the
    response handle

  • Client A can access now the response to its command without waiting
    (non-blocking)

The Client A takes advantage from not waiting on the result of the
command so it can process computational work or issue another Redis
command. The client can work with the command result as soon as the
response is available.

Impact of asynchronicity to the synchronous API

While this guide helps you to understand the asynchronous API it is
worthwhile to learn the impact on the synchronous API. The general
approach of the synchronous API is no different than the asynchronous
API. In both cases, the same facilities are used to invoke and transport
commands to the Redis server. The only difference is a blocking behavior
of the caller that is using the synchronous API. Blocking happens on
command level and affects only the command completion part, meaning
multiple clients using the synchronous API can invoke commands on the
same connection and at the same time without blocking each other. A call
on the synchronous API is unblocked at the moment a command response was
processed.

  • Given client A and client B

  • Client A triggers command SET A=B on the synchronous API and waits
    for the result

  • Client B triggers at the same time of Client A command SET C=D
    on the synchronous API and waits for the result

  • Redis receives command from Client A

  • Redis receives command from Client B

  • Redis processes SET A=B and responds OK to Client A

  • Client A receives the response and unblocks the program flow of
    Client A

  • Redis processes SET C=D and responds OK to Client B

  • Client B receives the response and unblocks the program flow of
    Client B

However, there are some cases you should not share a connection among
threads to avoid side-effects. The cases are:

  • Disabling flush-after-command to improve performance

  • The use of blocking operations like BLPOP. Blocking operations are
    queued on Redis until they can be executed. While one connection is
    blocked, other connections can issue commands to Redis. Once a command
    unblocks the blocking command (that said an LPUSH or RPUSH hits
    the list), the blocked connection is unblocked and can proceed after
    that.

  • Transactions

  • Using multiple databases

Result handles

Every command invocation on the asynchronous API creates a
RedisFuture<T> that can be canceled, awaited and subscribed
(listener). A CompleteableFuture<T> or RedisFuture<T> is a pointer
to the result that is initially unknown since the computation of its
value is yet incomplete. A RedisFuture<T> provides operations for
synchronization and chaining.

java
CompletableFuture<String> future = new CompletableFuture<>();

System.out.println("Current state: " + future.isDone());

future.complete("my value");

System.out.println("Current state: " + future.isDone());
System.out.println("Got value: " + future.get());

The example prints the following lines:

text
Current state: false
Current state: true
Got value: my value

Attaching a listener to a future allows chaining. Promises can be used
synonymous to futures, but not every future is a promise. A promise
guarantees a callback/notification and thus it has come to its name.

A simple listener that gets called once the future completes:

java
final CompletableFuture<String> future = new CompletableFuture<>();

future.thenRun(new Runnable() {
    @Override
    public void run() {
        try {
            System.out.println("Got value: " + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
});

System.out.println("Current state: " + future.isDone());
future.complete("my value");
System.out.println("Current state: " + future.isDone());

The value processing moves from the caller into a listener that is then
called by whoever completes the future. The example prints the following
lines:

text
Current state: false
Got value: my value
Current state: true

The code from above requires exception handling since calls to the
get() method can lead to exceptions. Exceptions raised during the
computation of the Future<T> are transported within an
ExecutionException. Another exception that may be thrown is the
InterruptedException. This is because calls to get() are blocking
calls and the blocked thread can be interrupted at any time. Just think
about a system shutdown.

The CompletionStage<T> type allows since Java 8 a much more
sophisticated handling of futures. A CompletionStage<T> can consume,
transform and build a chain of value processing. The code from above can
be rewritten in Java 8 in the following style:

java
CompletableFuture<String> future = new CompletableFuture<>();

future.thenAccept(new Consumer<String>() {
    @Override
    public void accept(String value) {
        System.out.println("Got value: " + value);
    }
});

System.out.println("Current state: " + future.isDone());
future.complete("my value");
System.out.println("Current state: " + future.isDone());

The example prints the following lines:

text
Current state: false
Got value: my value
Current state: true

You can find the full reference for the CompletionStage<T> type in the
Java 8 API
documentation
.

Creating futures using Lettuce

Lettuce futures can be used for initial and chaining operations. When
using Lettuce futures, you will notice the non-blocking behavior. This
is because all I/O and command processing are handled asynchronously
using the netty EventLoop. The Lettuce RedisFuture<T> extends a
CompletionStage<T> so all methods of the base type are available.

Lettuce exposes its futures on the Standalone, Sentinel,
Publish/Subscribe and Cluster APIs.

Connecting to Redis is insanely simple:

java
RedisClient client = RedisClient.create("redis://localhost");
RedisAsyncCommands<String, String> commands = client.connect().async();

In the next step, obtaining a value from a key requires the GET
operation:

java
RedisFuture<String> future = commands.get("key");

Consuming futures

The first thing you want to do when working with futures is to consume
them. Consuming a futures means obtaining the value. Here is an example
that blocks the calling thread and prints the value:

java
RedisFuture<String> future = commands.get("key");
String value = future.get();
System.out.println(value);

Invocations to the get() method (pull-style) block the calling thread
at least until the value is computed but in the worst case indefinitely.
Using timeouts is always a good idea to not exhaust your threads.

java
try {
    RedisFuture<String> future = commands.get("key");
    String value = future.get(1, TimeUnit.MINUTES);
    System.out.println(value);
} catch (Exception e) {
    e.printStackTrace();
}

The example will wait at most 1 minute for the future to complete. If
the timeout exceeds, a TimeoutException is thrown to signal the
timeout.

Futures can also be consumed in a push style, meaning when the
RedisFuture<T> is completed, a follow-up action is triggered:

java
RedisFuture<String> future = commands.get("key");

future.thenAccept(new Consumer<String>() {
    @Override
    public void accept(String value) {
        System.out.println(value);
    }
});

Alternatively, written in Java 8 lambdas:

java
RedisFuture<String> future = commands.get("key");

future.thenAccept(System.out::println);

Lettuce futures are completed on the netty EventLoop. Consuming and
chaining futures on the default thread is always a good idea except for
one case: Blocking/long-running operations. As a rule of thumb, never
block the event loop. If you need to chain futures using blocking calls,
use the thenAcceptAsync()/thenRunAsync() methods to fork the
processing to another thread. The โ€ฆโ€‹async() methods need a threading
infrastructure for execution, by default the ForkJoinPool.commonPool()
is used. The ForkJoinPool is statically constructed and does not grow
with increasing load. Using default Executors is almost always the
better idea.

java
Executor sharedExecutor = ...
RedisFuture<String> future = commands.get("key");

future.thenAcceptAsync(new Consumer<String>() {
    @Override
    public void accept(String value) {
        System.out.println(value);
    }
}, sharedExecutor);

Synchronizing futures

A key point when using futures is the synchronization. Futures are
usually used to:

  1. Trigger multiple invocations without the urge to wait for the
    predecessors (Batching)

  2. Invoking a command without awaiting the result at all (Fire&Forget)

  3. Invoking a command and perform other computing in the meantime
    (Decoupling)

  4. Adding concurrency to certain computational efforts (Concurrency)

There are several ways how to wait or get notified in case a future
completes. Certain synchronization techniques apply to some motivations
why you want to use futures.

Blocking synchronization

Blocking synchronization comes handy if you perform batching/add
concurrency to certain parts of your system. An example to batching can
be setting/retrieving multiple values and awaiting the results before a
certain point within processing.

java
List<RedisFuture<String>> futures = new ArrayList<RedisFuture<String>>();

for (int i = 0; i < 10; i++) {
    futures.add(commands.set("key-" + i, "value-" + i));
}

LettuceFutures.awaitAll(1, TimeUnit.MINUTES, futures.toArray(new RedisFuture[futures.size()]));

The code from above does not wait until a certain command completes
before it issues another one. The synchronization is done after all
commands are issued. The example code can easily be turned into a
Fire&Forget pattern by omitting the call to LettuceFutures.awaitAll().

A single future execution can be also awaited, meaning an opt-in to wait
for a certain time but without raising an exception:

java
RedisFuture<String> future = commands.get("key");

if(!future.await(1, TimeUnit.MINUTES)) {
    System.out.println("Could not complete within the timeout");
}

Calling await() is friendlier to call since it throws only an
InterruptedException in case the blocked thread is interrupted. You
are already familiar with the get() method for synchronization, so we
will not bother you with this one.

At last, there is another way to synchronize futures in a blocking way.
The major caveat is that you will become responsible to handle thread
interruptions. If you do not handle that aspect, you will not be able to
shut down your system properly if it is in a running state.

java
RedisFuture<String> future = commands.get("key");
while (!future.isDone()) {
    // do something ...
}

While the isDone() method does not aim primarily for synchronization
use, it might come handy to perform other computational efforts while
the command is executed.

Chaining synchronization

Futures can be synchronized/chained in a non-blocking style to improve
thread utilization. Chaining works very well in systems relying on
event-driven characteristics. Future chaining builds up a chain of one
or more futures that are executed serially, and every chain member
handles a part in the computation. The CompletionStage<T> API offers
various methods to chain and transform futures. A simple transformation
of the value can be done using the thenApply() method:

java
future.thenApply(new Function<String, Integer>() {
    @Override
    public Integer apply(String value) {
        return value.length();
    }
}).thenAccept(new Consumer() {
    @Override
    public void accept(Integer integer) {
        System.out.println("Got value: " + integer);
    }
});

Alternatively, written in Java 8 lambdas:

java
future.thenApply(String::length)
    .thenAccept(integer -> System.out.println("Got value: " + integer));

The thenApply() method accepts a function that transforms the value
into another one. The final thenAccept() method consumes the value for
final processing.

You have already seen the thenRun() method from previous examples. The
thenRun() method can be used to handle future completions in case the
data is not crucial to your flow:

java
future.thenRun(new Runnable() {
    @Override
    public void run() {
        System.out.println("Finished the future.");
    }
});

Keep in mind to execute the Runnable on a custom Executor if you are
doing blocking calls within the Runnable.

Another chaining method worth mentioning is the either-or chaining. A
couple of โ€ฆโ€‹Either() methods are available on a CompletionStage<T>,
see the Java 8 API docs
for the full reference. The either-or pattern consumes the value from
the first future that is completed. A good example might be two services
returning the same data, for instance, a Master-Replica scenario, but
you want to return the data as fast as possible:

java
RedisStringAsyncCommands<String, String> master = masterClient.connect().async();
RedisStringAsyncCommands<String, String> replica = replicaClient.connect().async();

RedisFuture<String> future = master.get("key");
future.acceptEither(replica.get("key"), new Consumer<String>() {
    @Override
    public void accept(String value) {
      System.out.println("Got value: " + value);
    }
});

Error handling

Error handling is an indispensable component of every real world
application and should to be considered from the beginning on. Futures
provide some mechanisms to deal with errors.

In general, you want to react in the following ways:

  • Return a default value instead

  • Use a backup future

  • Retry the future

RedisFuture<T>s transport exceptions if any occurred. Calls to the
get() method throw the occurred exception wrapped within an
ExecutionException (this is different to Lettuce 3.x). You can find
more details within the Javadoc on
CompletionStage.

The following code falls back to a default value after it runs to an
exception by using the handle() method:

java
future.handle(new BiFunction<String, Throwable, String>() {
    @Override
    public Integer apply(String value, Throwable throwable) {
        if(throwable != null) {
          return "default value";
        }
        return value;
    }
}).thenAccept(new Consumer<String>() {
    @Override
    public void accept(String value) {
        System.out.println("Got value: " + value);
    }
});

More sophisticated code could decide on behalf of the throwable type
that value to return, as the shortcut example using the
exceptionally() method:

java
future.exceptionally(new Function() {
    @Override
    public String apply(Throwable throwable) {
        if (throwable instanceof IllegalStateException) {
            return "default value";
        }

        return "other default value";
    }
});

Retrying futures and recovery using futures is not part of the Java 8
CompleteableFuture<T>. See the Reactive API for
comfortable ways handling with exceptions.

Examples

java
RedisAsyncCommands<String, String> async = client.connect().async();
RedisFuture<String> set = async.set("key", "value");
RedisFuture<String> get = async.get("key");

set.get() == "OK"
get.get() == "value"
java
RedisAsyncCommands<String, String> async = client.connect().async();
RedisFuture<String> set = async.set("key", "value");
RedisFuture<String> get = async.get("key");

set.await(1, SECONDS) == true
set.get() == "OK"
get.get(1, TimeUnit.MINUTES) == "value"
java
RedisStringAsyncCommands<String, String> async = client.connect().async();
RedisFuture<String> set = async.set("key", "value");

Runnable listener = new Runnable() {
    @Override
    public void run() {
            ...;
    }
};

set.thenRun(listener);

File: docs/user-guide/connecting-redis.md

Connecting Redis

Connections to a Redis Standalone, Sentinel, or Cluster require a
specification of the connection details. The unified form is RedisURI.
You can provide the database, password and timeouts within the
RedisURI. You have following possibilities to create a RedisURI:

  1. Use an URI:

    java
    RedisURI.create("redis://localhost/");
  2. Use the Builder

    java
    RedisURI.Builder.redis("localhost", 6379).auth("password").database(1).build();
  3. Set directly the values in RedisURI

    java
    new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);

URI syntax

Redis Standalone

text
redis :// [[username :] password@] host [:port][/database]
          [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&clientName=clientName]
          [&libraryName=libraryName] [&libraryVersion=libraryVersion] ]

Redis Standalone (SSL)

text
rediss :// [[username :] password@] host [: port][/database]
           [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&clientName=clientName]
           [&libraryName=libraryName] [&libraryVersion=libraryVersion] ]

Redis Standalone (Unix Domain Sockets)

text
redis-socket :// [[username :] password@]path
                 [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&database=database]
                 [&clientName=clientName] [&libraryName=libraryName]
                 [&libraryVersion=libraryVersion] ]

Redis Sentinel

text
redis-sentinel :// [[username :] password@] host1[:port1] [, host2[:port2]] [, hostN[:portN]] [/database]
                   [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&sentinelMasterId=sentinelMasterId]
                   [&clientName=clientName] [&libraryName=libraryName]
                   [&libraryVersion=libraryVersion] ]

Schemes

  • redis Redis Standalone

  • rediss Redis Standalone SSL

  • redis-socket Redis Standalone Unix Domain Socket

  • redis-sentinel Redis Sentinel

Timeout units

  • d Days

  • h Hours

  • m Minutes

  • s Seconds

  • ms Milliseconds

  • us Microseconds

  • ns Nanoseconds

Hint: The database parameter within the query part has higher precedence
than the database in the path.

RedisURI supports Redis Standalone, Redis Sentinel and Redis Cluster
with plain, SSL, TLS and unix domain socket connections.

Hint: The database parameter within the query part has higher precedence
than the database in the path. RedisURI supports Redis Standalone, Redis
Sentinel and Redis Cluster with plain, SSL, TLS and unix domain socket
connections.

Authentication

Redis URIs may contain authentication details that effectively lead to
usernames with passwords, password-only, or no authentication.
Connections are authenticated by using the information provided through
RedisCredentials. Credentials are obtained at connection time from
RedisCredentialsProvider. When configuring username/password on the
URI statically, then a StaticCredentialsProvider holds the configured
information.

Notes

  • When using Redis Sentinel, the password from the URI applies to the
    data nodes only. Sentinel authentication must be configured for each
    sentinel node.

  • Usernames are supported as of Redis 6.

  • Library name and library version are automatically set on Redis 7.2 or
    greater.

Basic Usage

java
RedisClient client = RedisClient.create("redis://localhost");          

StatefulRedisConnection<String, String> connection = client.connect(); 

RedisCommands<String, String> commands = connection.sync();            

String value = commands.get("foo");                                    

...

connection.close();                                                    

client.shutdown();
  • Create the RedisClient instance and provide a Redis URI pointing to
    localhost, Port 6379 (default port).

  • Open a Redis Standalone connection. The endpoint is used from the
    initialized RedisClient

  • Obtain the command API for synchronous execution. Lettuce supports
    asynchronous and reactive execution models, too.

  • Issue a GET command to get the key foo.

  • Close the connection when youโ€™re done. This happens usually at the
    very end of your application. Connections are designed to be
    long-lived.

  • Shut down the client instance to free threads and resources. This
    happens usually at the very end of your application.

Each Redis command is implemented by one or more methods with names
identical to the lowercase Redis command name. Complex commands with
multiple modifiers that change the result type include the CamelCased
modifier as part of the command name, e.g.ย zrangebyscore and
zrangebyscoreWithScores.

Redis connections are designed to be long-lived and thread-safe, and if
the connection is lost will reconnect until close() is called. Pending
commands that have not timed out will be (re)sent after successful
reconnection.

All connections inherit a default timeout from their RedisClient and
and will throw a RedisException when non-blocking commands fail to
return a result before the timeout expires. The timeout defaults to 60
seconds and may be changed in the RedisClient or for each connection.
Synchronous methods will throw a RedisCommandExecutionException in
case Redis responds with an error. Asynchronous connections do not throw
exceptions when Redis responds with an error.

RedisURI

The RedisURI contains the host/port and can carry
authentication/database details. On a successful connect you get
authenticated, and the database is selected afterward. This applies
also after re-establishing a connection after a connection loss.

A Redis URI can also be created from an URI string. Supported formats
are:

  • redis://[password@]host[:port][/databaseNumber] Plaintext Redis
    connection

  • rediss://[password@]host[:port][/databaseNumber] SSL
    Connections
    Redis connection

  • redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId
    for using Redis Sentinel

  • redis-socket:///path/to/socket Unix Domain
    Sockets
    connection to Redis

Exceptions

In the case of an exception/error response from Redis, youโ€™ll receive a
RedisException containing
the error message. RedisException is a RuntimeException.

Examples

java
RedisURI uri = new RedisURI("localhost", 6379, Duration.ofSeconds(20));
RedisClient client = RedisClient.create(uri);

// โ€ฆ

client.shutdown();
java
RedisURI redisUri = RedisURI.Builder.redis("localhost")
                                .withPassword("authentication")
                                .withDatabase(2)
                                .build();
RedisClient client = RedisClient.create(redisUri);

// โ€ฆ

client.shutdown();
java
RedisURI redisUri = RedisURI.Builder.redis("localhost")
                                .withSsl(true)
                                .withPassword("authentication")
                                .withDatabase(2)
                                .build();
RedisClient client = RedisClient.create(redisUri);

// โ€ฆ

client.shutdown();
java
RedisURI redisUri = RedisURI.create("redis://authentication@localhost/2");
RedisClient client = RedisClient.create(redisUri);

// โ€ฆ

client.shutdown();

Streaming Credentials Provider

Lettuce 6.6.0 extends RedisCredentialsProvider to support streaming credentials.
It is useful when you need to refresh credentials periodically. Example use cases include: token expiration, rotating credentials, etc.
Connection configured with RedisCredentialsProvider supporting streaming will be re-authenticated automatically when new credentials are emitted and ReauthenticateBehavior is set to ON_NEW_CREDENTIALS.

Step 1 - Create a Streaming Credentials Provider

A simple example of a streaming credentials provider that emits new credentials.

java
public class MyStreamingRedisCredentialsProvider implements RedisCredentialsProvider {

    private final Sinks.Many<RedisCredentials> credentialsSink = Sinks.many().replay().latest();

    @Override
    public boolean supportsStreaming() {
        return true;
    }

    @Override
    public Mono<RedisCredentials> resolveCredentials() {
        return credentialsSink.asFlux().next();
    }
    
    @Override
    // Provide a continuous stream of credentials
    public Flux<RedisCredentials> credentials() {
        return credentialsSink.asFlux().onBackpressureLatest(); 
    }

    public void close() {
        credentialsSink.tryEmitComplete();
    }
    // Emit new credentials when needed
    public void emitCredentials(String username, char[] password) {
        credentialsSink.tryEmitNext(new StaticRedisCredentials(username, password));
    }

}

Step 2 - Create a RedisURI with streaming credentials provider

java
// Create a streaming credentials provider
    MyStreamingRedisCredentialsProvider streamingCredentialsProvider = new MyStreamingRedisCredentialsProvider();
    
    // Emit initial credentials
    streamingCredentialsProvider.emitCredentials("testuser", "testpass".toCharArray());
   
    // Enable automatic re-authentication
    ClientOptions clientOptions = ClientOptions.builder()
            // enable automatic re-authentication
            .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS)
            .build();
    
    // Create a RedisURI with streaming credentials provider
    RedisURI redisURI = RedisURI.builder().withHost(HOST).withPort(PORT)
            .withAuthentication(streamingCredentialsProvider)
            .build();
    
    // RedisClient
    RedisClient redisClient = RedisClient.create(redisURI);
    rediscClient.connect().sync().ping();
    
    // ...
    // Emit new credentials when needed
    streamingCredentialsProvider.emitCredentials("testuser", "password-rotated".toCharArray());

Microsoft Entra ID Authentication

Lettuce 6.6.0 introduces built-in support for authentication with Azure Managed Redis and Azure Cache for Redis using Microsoft Entra ID (formerly Azure Active Directory). It enables seamless integration with Azure's Redis services by fetching authentication tokens and managing the token renewal in the background.
Integration is built on top of redis-authx library, and provides support for:

  • System-assigned managed identity
  • User-assigned managed identity
  • Service principal

You can learn more about managed identities in the Microsoft Entra ID documentation.

Basic Usage

Pre-requisites

Step 1 - Add the dependencies

Lettuce requires redis-authx-entraid dependency to provide Microsoft Entra ID authentication support. Make sure to include that dependency on your classpath.

If using Maven, add the following dependency to your pom.xml:

xml
<dependency>
            <groupId>redis.clients.authentication</groupId>
            <artifactId>redis-authx-entraid</artifactId>
            <version>0.1.1-beta1</version>
        </dependency>

Step 2 - Create Entra ID enabled credentials provider

The lifecycle of the credentials provider is not managed by the Lettuce client. You can create it once and reuse it across multiple clients\connections. When no longer needed, you should close the provider to release resources TokenBasedRedisCredentialsProvider#close.

Create Microsoft Entra ID enabled credentials provider

java
// Entra ID enabled credentials provider for Service Principle Identity with Client Secret
  TokenBasedRedisCredentialsProvider credentialsSP;
  try ( EntraIDTokenAuthConfigBuilder builder = EntraIDTokenAuthConfigBuilder.builder()) {
       builder.clientId(CLIENT_ID)
              .secret(CLIENT_SECRET)
              .authority(AUTHORITY) // "https://login.microsoftonline.com/{YOUR_TENANT_ID}";
              .scopes(SCOPES);      // "https://redis.azure.com/.default";
      credentialsSP = TokenBasedRedisCredentialsProvider.create(builder.build());
  }

You can test the credentials provider by obtaining a token.

java
// Test Entra ID credentials provider can resolve credentials
  credentialsSP.resolveCredentials()
      .doOnNext(c-> System.out.println(c.getUsername()))
      .block();

Step 3 - Enable automatic re-authentication

Microsoft Entra ID tokens have a limited lifetime. Lettuce provides a mechanism to automatically re-authenticate when new credentials are emitted by a RedisCredentialsProvider.

java
// Enable automatic re-authentication
  ClientOptions clientOptions = ClientOptions.builder()
        .reauthenticateBehavior(ClientOptions.ReauthenticateBehavior.ON_NEW_CREDENTIALS)
        .build();

Step 4 - Connect with Entra ID enabled credentials provider

java
// Use Entra ID credentials provider
  RedisURI redisURI = RedisURI.builder()
      .withHost(HOST)
      .withPort(PORT)
      .withSsl(true)
      .withAuthentication(credentialsSP).build();

  // RedisClient
  RedisClient redisClient = RedisClient.create(redisURI);
  redisClient.setOptions(clientOptions);
  
  try {
      redisClient.setOptions(clientOptions);
      // Connect with Entra ID credentials provider
      try (StatefulRedisConnection<String, String> user1 = redisClient.connect(StringCodec.UTF8)) {
          System.out.println("Connected to redis as :" + user1.sync().aclWhoami());
          System.out.println("Db size :" + user1.sync().dbsize());
      }
  } finally {
      redisClient.shutdown();  // Shutdown Redis client and close connections
      credentialsSP.close(); // Shutdown Entra ID Credentials provider
  }

File: docs/user-guide/kotlin-api.md

Kotlin API

Kotlin Coroutines are using Kotlin lightweight threads allowing to write
non-blocking code in an imperative way. On language side, suspending
functions provides an abstraction for asynchronous operations while on
library side kotlinx.coroutines provides functions like async { } and
types like Flow.

Lettuce ships with extensions to provide support for idiomatic Kotlin
use.

Dependencies

Coroutines support is available when kotlinx-coroutines-core and
kotlinx-coroutines-reactive dependencies are on the classpath:

xml
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-reactive</artifactId>
    <version>${kotlinx-coroutines.version}</version>
</dependency>

How does Reactive translate to Coroutines?

Flow is an equivalent to Flux in Coroutines world, suitable for hot
or cold streams, finite or infinite streams, with the following main
differences:

  • Flow is push-based while Flux is a push-pull hybrid

  • Backpressure is implemented via suspending functions

  • Flow has only a single suspending collect method and operators are
    implemented as extensions

  • Operators are easy to implement thanks to Coroutines

  • Extensions allow to add custom operators to Flow

  • Collect operations are suspending functions

  • map operator supports asynchronous operations (no need for
    flatMap) since it takes a suspending function parameter

Coroutines API based on reactive operations

Example for retrieving commands and using it:

kotlin
val api: RedisCoroutinesCommands<String, String> = connection.coroutines()

val foo1 = api.set("foo", "bar")
val foo2 = api.keys("fo*")

!!! NOTE
Coroutine Extensions are experimental and require opt-in using
@ExperimentalLettuceCoroutinesApi. The API ships with a reduced
feature set. Deprecated methods and StreamingChannel are left out
intentionally. Expect evolution towards a Flow-based API to consume
large Redis responses.

Extensions for existing APIs

Transactions DSL

Example for the synchronous API:

kotlin
val result: TransactionResult = connection.sync().multi {
    set("foo", "bar")
    get("foo")
}

Example for async with coroutines:

kotlin
val result: TransactionResult = connection.async().multi {
    set("foo", "bar")
    get("foo")
}

File: docs/user-guide/lua-scripting.md

Lua Scripting

Lua is a powerful scripting language
that is supported at the core of Redis. Lua scripts can be invoked
dynamically by providing the script contents to Redis or used as stored
procedure by loading the script into Redis and using its digest to
invoke it.

java
String helloWorld = redis.eval("return ARGV[1]", STATUS, new String[0], "Hello World");

Using Lua scripts is straightforward. Consuming results in Java requires
additional details to consume the result through a matching type. As we
do not know what your script will return, the API uses call-site
generics for you to specify the result type. Additionally, you must
provide a ScriptOutputType hint to EVAL so that the driver uses the
appropriate output parser. See Output Formats for
further details.

Lua scripts can be stored on the server for repeated execution.
Dynamically-generated scripts are an anti-pattern as each script is
stored in Redis' script cache. Generating scripts during the application
runtime may, and probably will, exhaust the hostโ€™s memory resources for
caching them. Instead, scripts should be as generic as possible and
provide customized execution via their arguments. You can register a
script through SCRIPT LOAD and use its SHA digest to invoke it later:

java
String digest = redis.scriptLoad("return ARGV[1]", STATUS, new String[0], "Hello World");

// later
String helloWorld = redis.evalsha(digest, STATUS, new String[0], "Hello World");

File: docs/user-guide/pubsub.md

Publish/Subscribe

Lettuce provides support for Publish/Subscribe on Redis Standalone and
Redis Cluster connections. The connection is notified on
message/subscribed/unsubscribed events after subscribing to channels or
patterns. Synchronous, asynchronous
and reactive APIs are provided to interact with Redis
Publish/Subscribe features.

Subscribing

A connection can notify multiple listeners that implement
RedisPubSubListener (Lettuce provides a RedisPubSubAdapter for
convenience). All listener registrations are kept within the
StatefulRedisPubSubConnection/StatefulRedisClusterConnection.

java
StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub()
connection.addListener(new RedisPubSubListener<String, String>() { ... })

RedisPubSubCommands<String, String> sync = connection.sync();
sync.subscribe("channel");

// application flow continues

!!! NOTE
Donโ€™t issue blocking calls (includes synchronous API calls to Lettuce)
from inside of Pub/Sub callbacks as this would block the EventLoop. If
you need to fetch data from Redis from inside a callback, please use
the asynchronous API.

java
StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub()
connection.addListener(new RedisPubSubListener<String, String>() { ... })

RedisPubSubAsyncCommands<String, String> async = connection.async();
RedisFuture<Void> future = async.subscribe("channel");

// application flow continues

Reactive API

The reactive API provides hot Observables to listen on
ChannelMessages and PatternMessages. The Observables receive all
inbound messages. You can do filtering using the observable chain if you
need to filter out the interesting ones, The Observable stops
triggering events when the subscriber unsubscribes from it.

java
StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub()

RedisPubSubReactiveCommands<String, String> reactive = connection.reactive();
reactive.subscribe("channel").subscribe();

reactive.observeChannels().doOnNext(patternMessage -> {...}).subscribe()

// application flow continues

Redis Cluster

Redis Cluster support Publish/Subscribe but requires some attention in
general. User-space Pub/Sub messages (Calling PUBLISH) are broadcasted
across the whole cluster regardless of subscriptions to particular
channels/patterns. This behavior allows connecting to an arbitrary
cluster node and registering a subscription. The client isnโ€™t required
to connect to the node where messages were published.

A cluster-aware Pub/Sub connection is provided by
RedisClusterClient.connectPubSub() allowing to listen for cluster
reconfiguration and reconnect if the topology changes.

java
StatefulRedisClusterPubSubConnection<String, String> connection = clusterClient.connectPubSub()
connection.addListener(new RedisPubSubListener<String, String>() { ... })

RedisPubSubCommands<String, String> sync = connection.sync();
sync.subscribe("channel");

Redis Cluster also makes a distinction between user-space and key-space
messages. Key-space notifications (Pub/Sub messages for key-activity)
stay node-local and are not broadcasted across the Redis Cluster. A
notification about, e.g.ย an expiring key, stays local to the node on
which the key expired.

Clients that are interested in keyspace notifications must subscribe to
the appropriate node (or nodes) to receive these notifications. You can
either use RedisClient.connectPubSub() to establish Pub/Sub
connections to the individual nodes or use RedisClusterClient's
message propagation and NodeSelection API to get a managed set of
connections.

java
StatefulRedisClusterPubSubConnection<String, String> connection = clusterClient.connectPubSub()
connection.addListener(new RedisClusterPubSubListener<String, String>() { ... })
connection.setNodeMessagePropagation(true);

RedisPubSubCommands<String, String> sync = connection.sync();
sync.masters().commands().subscribe("__keyspace@0__:*");

There are two things to pay special attention to:

  1. Replication: Keys replicated to replica nodes, especially
    considering expiry, generate keyspace events on all nodes holding
    the key. If a key expires and it is replicated, it will expire on
    the master and all replicas. Each Redis server will emit keyspace
    events. Subscribing to non-master nodes, therefore, will let your
    application see multiple events of the same type for the same key
    because of Redis distributed nature.

  2. Topology Changes: Subscriptions are issued either by using the
    NodeSelection API or by calling subscribe(โ€ฆ) on the individual
    cluster node connections. Subscription registrations are not
    propagated to new nodes that are added on a topology change.


File: docs/user-guide/reactive-api.md

Reactive API

This guide helps you to understand the Reactive Stream pattern and aims
to give you a general understanding of how to build reactive
applications.

Motivation

Asynchronous and reactive methodologies allow you to utilize better
system resources, instead of wasting threads waiting for network or disk
I/O. Threads can be fully utilized to perform other work instead.

A broad range of technologies exists to facilitate this style of
programming, ranging from the very limited and less usable
java.util.concurrent.Future to complete libraries and runtimes like
Akka. Project Reactor, has a very rich set
of operators to compose asynchronous workflows, it has no further
dependencies to other frameworks and supports the very mature Reactive
Streams model.

Understanding Reactive Streams

Reactive Streams is an initiative to provide a standard for asynchronous
stream processing with non-blocking back pressure. This encompasses
efforts aimed at runtime environments (JVM and JavaScript) as well as
network protocols.

The scope of Reactive Streams is to find a minimal set of interfaces,
methods, and protocols that will describe the necessary operations and
entities to achieve the goalโ€”asynchronous streams of data with
non-blocking back pressure.

It is an interoperability standard between multiple reactive composition
libraries that allow interaction without the need of bridging between
libraries in application code.

The integration of Reactive Streams is usually accompanied with the use
of a composition library that hides the complexity of bare
Publisher<T> and Subscriber<T> types behind an easy-to-use API.
Lettuce uses Project Reactor that exposes
its publishers as Mono and Flux.

For more information about Reactive Streams see
http://reactive-streams.org.

Understanding Publishers

Asynchronous processing decouples I/O or computation from the thread
that invoked the operation. A handle to the result is given back,
usually a java.util.concurrent.Future or similar, that returns either
a single object, a collection or an exception. Retrieving a result, that
was fetched asynchronously is usually not the end of processing one
flow. Once data is obtained, further requests can be issued, either
always or conditionally. With Java 8 or the Promise pattern, linear
chaining of futures can be set up so that subsequent asynchronous
requests are issued. Once conditional processing is needed, the
asynchronous flow has to be interrupted and synchronized. While this
approach is possible, it does not fully utilize the advantage of
asynchronous processing.

In contrast to the preceding examples, Publisher<T> objects answer the
multiplicity and asynchronous questions in a different fashion: By
inverting the Pull pattern into a Push pattern.

A Publisher is the asynchronous/push โ€œdualโ€ to the synchronous/pull
Iterable

event Iterable (pull) Publisher (push)
retrieve data T next() onNext(T)
discover error throws Exception onError(Exception)
complete !hasNext() onCompleted()

An Publisher<T> supports emission sequences of values or even infinite
streams, not just the emission of single scalar values (as Futures do).
You will very much appreciate this fact once you start to work on
streams instead of single values. Project Reactor uses two types in its
vocabulary: Mono and Flux that are both publishers.

A Mono can emit 0 to 1 events while a Flux can emit 0 to N
events.

A Publisher<T> is not biased toward some particular source of
concurrency or asynchronicity and how the underlying code is executed -
synchronous or asynchronous, running within a ThreadPool. As a
consumer of a Publisher<T>, you leave the actual implementation to the
supplier, who can change it later on without you having to adapt your
code.

The last key point of a Publisher<T> is that the underlying processing
is not started at the time the Publisher<T> is obtained, rather its
started at the moment an observer subscribes or signals demand to the
Publisher<T>. This is a crucial difference to a
java.util.concurrent.Future, which is started somewhere at the time it
is created/obtained. So if no observer ever subscribes to the
Publisher<T>, nothing ever will happen.

A word on the lettuce Reactive API

All commands return a Flux<T>, Mono<T> or Mono<Void> to which a
Subscriber can subscribe to. That subscriber reacts to whatever item
or sequence of items the Publisher<T> emits. This pattern facilitates
concurrent operations because it does not need to block while waiting
for the Publisher<T> to emit objects. Instead, it creates a sentry in
the form of a Subscriber that stands ready to react appropriately at
whatever future time the Publisher<T> does so.

Consuming `Publisher`

The first thing you want to do when working with publishers is to
consume them. Consuming a publisher means subscribing to it. Here is an
example that subscribes and prints out all the items emitted:

java
Flux.just("Ben", "Michael", "Mark").subscribe(new Subscriber<String>() {
    public void onSubscribe(Subscription s) {
        s.request(3);
    }

    public void onNext(String s) {
        System.out.println("Hello " + s + "!");
    }

    public void onError(Throwable t) {

    }

    public void onComplete() {
        System.out.println("Completed");
    }
});

The example prints the following lines:

text
Hello Ben
Hello Michael
Hello Mark
Completed

You can see that the Subscriber (or Observer) gets notified of every
event and also receives the completed event. A Publisher<T> emits
items until either an exception is raised or the Publisher<T> finishes
the emission calling onCompleted. No further elements are emitted
after that time.

A call to the subscribe registers a Subscription that allows to
cancel and, therefore, do not receive further events. Publishers can
interoperate with the un-subscription and free resources once a
subscriber unsubscribed from the Publisher<T>.

Implementing a Subscriber<T> requires implementing numerous methods,
so lets rewrite the code to a simpler form:

java
Flux.just("Ben", "Michael", "Mark").doOnNext(new Consumer<String>() {
    public void accept(String s) {
        System.out.println("Hello " + s + "!");
    }
}).doOnComplete(new Runnable() {
    public void run() {
        System.out.println("Completed");
    }
}).subscribe();

alternatively, even simpler by using Java 8 Lambdas:

java
Flux.just("Ben", "Michael", "Mark")
        .doOnNext(s -> System.out.println("Hello " + s + "!"))
        .doOnComplete(() -> System.out.println("Completed"))
        .subscribe();

You can control the elements that are processed by your Subscriber
using operators. The take() operator limits the number of emitted
items if you are interested in the first N elements only.

java
Flux.just("Ben", "Michael", "Mark") //
        .doOnNext(s -> System.out.println("Hello " + s + "!"))
        .doOnComplete(() -> System.out.println("Completed"))
        .take(2)
        .subscribe();

The example prints the following lines:

text
Hello Ben
Hello Michael
Completed

Note that the take operator implicitly cancels its subscription from
the Publisher<T> once the expected count of elements was emitted.

A subscription to a Publisher<T> can be done either by another Flux
or a Subscriber. Unless you are implementing a custom Publisher,
always use Subscriber. The used subscriber Consumer from the example
above does not handle Exceptions so once an Exception is thrown you
will see a stack trace like this:

text
Exception in thread "main" reactor.core.Exceptions$BubblingException: java.lang.RuntimeException: Example exception
    at reactor.core.Exceptions.bubble(Exceptions.java:96)
    at reactor.core.publisher.Operators.onErrorDropped(Operators.java:296)
    at reactor.core.publisher.LambdaSubscriber.onError(LambdaSubscriber.java:117)
    ...
Caused by: java.lang.RuntimeException: Example exception
    at demos.lambda$example3Lambda$4(demos.java:87)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:157)
    ... 23 more

It is always recommended to implement an error handler right from the
beginning. At a certain point, things can and will go wrong.

A fully implemented subscriber declares the onCompleted and onError
methods allowing you to react to these events:

java
Flux.just("Ben", "Michael", "Mark").subscribe(new Subscriber<String>() {
    public void onSubscribe(Subscription s) {
        s.request(3);
    }

    public void onNext(String s) {
        System.out.println("Hello " + s + "!");
    }

    public void onError(Throwable t) {
        System.out.println("onError: " + t);
    }

    public void onComplete() {
        System.out.println("Completed");
    }
});

From push to pull

The examples from above illustrated how publishers can be set up in a
not-opinionated style about blocking or non-blocking execution. A
Flux<T> can be converted explicitly into an Iterable<T> or
synchronized with block(). Avoid calling block() in your code as you
start expressing the nature of execution inside your code. Calling
block() removes all non-blocking advantages of the reactive chain to
your application.

java
String last = Flux.just("Ben", "Michael", "Mark").last().block();
System.out.println(last);

The example prints the following line:

text
Mark

A blocking call can be used to synchronize the publisher chain and find
back a way into the plain and well-known Pull pattern.

java
List<String> list = Flux.just("Ben", "Michael", "Mark").collectList().block();
System.out.println(list);

The toList operator collects all emitted elements and passes the list
through the BlockingPublisher<T>.

The example prints the following line:

text
[Ben, Michael, Mark]

Creating `Flux` and `Mono` using Lettuce

There are many ways to establish publishers. You have already seen
just(), take() and collectList(). Refer to the Project Reactor
documentation
for many more methods
that you can use to create Flux and Mono.

Lettuce publishers can be used for initial and chaining operations. When
using Lettuce publishers, you will notice the non-blocking behavior.
This is because all I/O and command processing are handled
asynchronously using the netty EventLoop.

Connecting to Redis is insanely simple:

java
RedisClient client = RedisClient.create("redis://localhost");
RedisStringReactiveCommands<String, String> commands = client.connect().reactive();

In the next step, obtaining a value from a key requires the GET
operation:

java
commands.get("key").subscribe(new Consumer<String>() {

    public void accept(String value) {
        System.out.println(value);
    }
});

Alternatively, written in Java 8 lambdas:

java
commands
   .get("key")
   .subscribe(value -> System.out.println(value));

The execution is handled asynchronously, and the invoking Thread can be
used to processed in processing while the operation is completed on the
Netty EventLoop threads. Due to its decoupled nature, the calling method
can be left before the execution of the Publisher<T> is finished.

Lettuce publishers can be used within the context of chaining to load
multiple keys asynchronously:

java
Flux.just("Ben", "Michael", "Mark").
        flatMap(key -> commands.get(key)).
        subscribe(value -> System.out.println("Got value: " + value));

Hot and Cold Publishers

There is a distinction between Publishers that was not covered yet:

  • A cold Publishers waits for a subscription until it emits values and
    does this freshly for every subscriber.

  • A hot Publishers begins emitting values upfront and presents them to
    every subscriber subsequently.

All Publishers returned from the Redis Standalone, Redis Cluster, and
Redis Sentinel API are cold, meaning that no I/O happens until they are
subscribed to. As such a subscriber is guaranteed to see the whole
sequence from the beginning. So just creating a Publisher will not cause
any network I/O thus creating and discarding Publishers is cheap.
Publishers created for a Publish/Subscribe emit PatternMessages and
ChannelMessages once they are subscribed to. Publishers guarantee
however to emit all items from the beginning until their end. While this
is true for Publish/Subscribe publishers, the nature of subscribing to a
Channel/Pattern allows missed messages due to its subscription nature
and less to the Hot/Cold distinction of publishers.

Transforming publishers

Publishers can transform the emitted values in various ways. One of the
most basic transformations is flatMap() which you have seen from the
examples above that converts the incoming value into a different one.
Another one is map(). The difference between map() and flatMap()
is that flatMap() allows you to do those transformations with
Publisher<T> calls.

java
Flux.just("Ben", "Michael", "Mark")
        .flatMap(commands::get)
        .flatMap(value -> commands.rpush("result", value))
        .subscribe();

The first flatMap() function is used to retrieve a value and the
second flatMap() function appends the value to a Redis list named
result. The flatMap() function returns a Publisher whereas the
normal map just returns <T>. You will use flatMap() a lot when
dealing with flows like this, youโ€™ll become good friends.

An aggregation of values can be achieved using the reduce()
transformation. It applies a function to each value emitted by a
Publisher<T>, sequentially and emits each successive value. We can use
it to aggregate values, to count the number of elements in multiple
Redis sets:

java
Flux.just("Ben", "Michael", "Mark")
        .flatMap(commands::scard)
        .reduce((sum, current) -> sum + current)
        .subscribe(result -> System.out.println("Number of elements in sets: " + result));

The aggregation function of reduce() is applied on each emitted value,
so three times in the example above. If you want to get the last value,
which denotes the final result containing the number of elements in all
Redis sets, apply the last() transformation:

java
Flux.just("Ben", "Michael", "Mark")
        .flatMap(commands::scard)
        .reduce((sum, current) -> sum + current)
        .last()
        .subscribe(result -> System.out.println("Number of elements in sets: " + result));

Now letโ€™s take a look at grouping emitted items. The following example
emits three items and groups them by the beginning character.

java
Flux.just("Ben", "Michael", "Mark")
    .groupBy(key -> key.substring(0, 1))
    .subscribe(
        groupedFlux -> {
            groupedFlux.collectList().subscribe(list -> {
                System.out.println("First character: " + groupedFlux.key() + ", elements: " + list);
            });
        }
);

The example prints the following lines:

text
First character: B, elements: [Ben]
First character: M, elements: [Michael, Mark]

Absent values

The presence and absence of values is an essential part of reactive
programming. Traditional approaches consider null as an absence of a
particular value. With Java 8, Optional<T> was introduced to
encapsulate nullability. Reactive Streams prohibits the use of null
values.

In the scope of Redis, an absent value is an empty list, a non-existent
key or any other empty data structure. Reactive programming discourages
the use of null as value. The reactive answer to absent values is just
not emitting any value that is possible due the 0 to N nature of
Publisher<T>.

Suppose we have the keys Ben and Michael set each to the value
value. We query those and another, absent key with the following code:

java
Flux.just("Ben", "Michael", "Mark")
        .flatMap(commands::get)
        .doOnNext(value -> System.out.println(value))
        .subscribe();

The example prints the following lines:

text
value
value

The output is just two values. The GET to the absent key Mark does
not emit a value.

The reactive API provides operators to work with empty results when you
require a value. You can use one of the following operators:

  • defaultIfEmpty: Emit a default value if the Publisher<T> did not
    emit any value at all

  • switchIfEmpty: Switch to a fallback Publisher<T> to emit values

  • Flux.hasElements/Flux.hasElement: Emit a Mono that
    contains a flag whether the original Publisher<T> is empty

  • next/last/elementAt: Positional operators to retrieve the
    first/last/Nth element or emit a default value

Filtering items

The values emitted by a Publisher<T> can be filtered in case you need
only specific results. Filtering does not change the emitted values
itself. Filters affect how many items and at which point (and if at all)
they are emitted.

java
Flux.just("Ben", "Michael", "Mark")
        .filter(s -> s.startsWith("M"))
        .flatMap(commands::get)
        .subscribe(value -> System.out.println("Got value: " + value));

The code will fetch only the keys Michael and Mark but not Ben.
The filter criteria are whether the key starts with a M.

You already met the last() filter to retrieve the last value:

java
Flux.just("Ben", "Michael", "Mark")
        .last()
        .subscribe(value -> System.out.println("Got value: " + value));

the extended variant of last() allows you to take the last N values:

java
Flux.just("Ben", "Michael", "Mark")
        .takeLast(3)
        .subscribe(value -> System.out.println("Got value: " + value));

The example from above takes the last 2 values.

The opposite to next() is the first() filter that is used to
retrieve the next value:

java
Flux.just("Ben", "Michael", "Mark")
        .next()
        .subscribe(value -> System.out.println("Got value: " + value));

Error handling

Error handling is an indispensable component of every real world
application and should to be considered from the beginning on. Project
Reactor provides several mechanisms to deal with errors.

In general, you want to react in the following ways:

  • Return a default value instead

  • Use a backup publisher

  • Retry the Publisher (immediately or with delay)

The following code falls back to a default value after it throws an
exception at the first emitted item:

java
Flux.just("Ben", "Michael", "Mark")
        .doOnNext(value -> {
            throw new IllegalStateException("Takes way too long");
        })
        .onErrorReturn("Default value")
        .subscribe();

You can use a backup Publisher<T> which will be called if the first
one fails.

java
Flux.just("Ben", "Michael", "Mark")
        .doOnNext(value -> {
            throw new IllegalStateException("Takes way too long");
        })
        .switchOnError(commands.get("Default Key"))
        .subscribe();

It is possible to retry the publisher by re-subscribing. Re-subscribing
can be done as soon as possible, or with a wait interval, which is
preferred when external resources are involved.

java
Flux.just("Ben", "Michael", "Mark")
        .flatMap(commands::get)
        .retry()
        .subscribe();

Use the following code if you want to retry with backoff:

java
Flux.just("Ben", "Michael", "Mark")
        .doOnNext(v -> {
            if (new Random().nextInt(10) + 1 == 5) {
                throw new RuntimeException("Boo!");
            }
        })
        .doOnSubscribe(subscription ->
        {
            System.out.println(subscription);
        })
        .retryWhen(throwableFlux -> Flux.range(1, 5)
                .flatMap(i -> {
                    System.out.println(i);
                    return Flux.just(i)
                            .delay(Duration.of(i, ChronoUnit.SECONDS));
                }))
        .blockLast();

The attempts get passed into the retryWhen() method delayed with the
number of seconds to wait. The delay method is used to complete once its
timer is done.

Schedulers and threads

Schedulers in Project Reactor are used to instruct multi-threading. Some
operators have variants that take a Scheduler as a parameter. These
instruct the operator to do some or all of its work on a particular
Scheduler.

Project Reactor ships with a set of preconfigured Schedulers, which are
all accessible through the Schedulers class:

  • Schedulers.parallel(): Executes the computational work such as
    event-loops and callback processing.

  • Schedulers.immediate(): Executes the work immediately in the current
    thread

  • Schedulers.elastic(): Executes the I/O-bound work such as asynchronous
    performance of blocking I/O, this scheduler is backed by a thread-pool
    that will grow as needed

  • Schedulers.newSingle(): Executes the work on a new thread

  • Schedulers.fromExecutor(): Create a scheduler from a
    java.util.concurrent.Executor

  • Schedulers.timer(): Create or reuse a hash-wheel based TimedScheduler
    with a resolution of 50ms.

Do not use the computational scheduler for I/O.

Publishers can be executed by a scheduler in the following different
ways:

  • Using an operator that makes use of a scheduler

  • Explicitly by passing the Scheduler to such an operator

  • By using subscribeOn(Scheduler)

  • By using publishOn(Scheduler)

Operators like buffer, replay, skip, delay, parallel, and so
forth use a Scheduler by default if not instructed otherwise.

All of the listed operators allow you to pass in a custom scheduler if
needed. Sticking most of the time with the defaults is a good idea.

If you want the subscribe chain to be executed on a specific scheduler,
you use the subscribeOn() operator. The code is executed on the main
thread without a scheduler set:

java
Flux.just("Ben", "Michael", "Mark").flatMap(key -> {
            System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")");
            return Flux.just(key);
        }
).flatMap(value -> {
            System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")");
            return Flux.just(value);
        }
).subscribe();

The example prints the following lines:

text
Map 1: Ben (main)
Map 2: Ben (main)
Map 1: Michael (main)
Map 2: Michael (main)
Map 1: Mark (main)
Map 2: Mark (main)

This example shows the subscribeOn() method added to the flow (it does
not matter where you add it):

java
Flux.just("Ben", "Michael", "Mark").flatMap(key -> {
            System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")");
            return Flux.just(key);
        }
).flatMap(value -> {
            System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")");
            return Flux.just(value);
        }
).subscribeOn(Schedulers.parallel()).subscribe();

The output of the example shows the effect of subscribeOn(). You can
see that the Publisher is executed on the same thread, but on the
computation thread pool:

text
Map 1: Ben (parallel-1)
Map 2: Ben (parallel-1)
Map 1: Michael (parallel-1)
Map 2: Michael (parallel-1)
Map 1: Mark (parallel-1)
Map 2: Mark (parallel-1)

If you apply the same code to Lettuce, you will notice a difference in
the threads on which the second flatMap() is executed:

java
Flux.just("Ben", "Michael", "Mark").flatMap(key -> {
    System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")");
    return commands.set(key, key);
}).flatMap(value -> {
    System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")");
    return Flux.just(value);
}).subscribeOn(Schedulers.parallel()).subscribe();

The example prints the following lines:

text
Map 1: Ben (parallel-1)
Map 1: Michael (parallel-1)
Map 1: Mark (parallel-1)
Map 2: OK (lettuce-nioEventLoop-3-1)
Map 2: OK (lettuce-nioEventLoop-3-1)
Map 2: OK (lettuce-nioEventLoop-3-1)

Two things differ from the standalone examples:

  1. The values are set rather concurrently than sequentially

  2. The second flatMap() transformation prints the netty EventLoop
    thread name

This is because Lettuce publishers are executed and completed on the
netty EventLoop threads by default.

publishOn instructs an Publisher to call its observerโ€™s onNext,
onError, and onCompleted methods on a particular Scheduler. Here,
the order matters:

java
Flux.just("Ben", "Michael", "Mark").flatMap(key -> {
    System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")");
    return commands.set(key, key);
}).publishOn(Schedulers.parallel()).flatMap(value -> {
    System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")");
    return Flux.just(value);
}).subscribe();

Everything before the publishOn() call is executed in main, everything
below in the scheduler:

text
Map 1: Ben (main)
Map 1: Michael (main)
Map 1: Mark (main)
Map 2: OK (parallel-1)
Map 2: OK (parallel-1)
Map 2: OK (parallel-1)

Schedulers allow direct scheduling of operations. Refer to the Project
Reactor
documentation

for further information.

Redis Transactions

Lettuce provides a convenient way to use Redis Transactions in a
reactive way. Commands that should be executed within a transaction can
be executed after the MULTI command was executed. Functional chaining
allows to execute commands within a closure, and each command receives
its appropriate response. A cumulative response is also returned with
TransactionResult in response to EXEC.

See Transactions for
further details.

Other examples

Blocking example

java
RedisStringReactiveCommands<String, String> reactive = client.connect().reactive();
Mono<String> set = reactive.set("key", "value");
set.block();

Non-blocking example

java
RedisStringReactiveCommands<String, String> reactive = client.connect().reactive();
Mono<String> set = reactive.set("key", "value");
set.subscribe();

Functional chaining

java
RedisStringReactiveCommands<String, String> reactive = client.connect().reactive();
Flux.just("Ben", "Michael", "Mark")
        .flatMap(key -> commands.sadd("seen", key))
        .flatMap(value -> commands.randomkey())
        .flatMap(commands::type)
        .doOnNext(System.out::println).subscribe();

Redis Transaction

java
RedisReactiveCommands<String, String> reactive = client.connect().reactive();

    reactive.multi().doOnSuccess(s -> {
        reactive.set("key", "1").doOnNext(s1 -> System.out.println(s1)).subscribe();
        reactive.incr("key").doOnNext(s1 -> System.out.println(s1)).subscribe();
    }).flatMap(s -> reactive.exec())
            .doOnNext(transactionResults -> System.out.println(transactionResults.wasRolledBack()))
            .subscribe();

File: docs/user-guide/redis-functions.md

Redis Functions

Redis Functions is an
evolution of the scripting API to provide extensibility beyond Lua.
Functions can leverage different engines and follow a model where a
function library registers functionality to be invoked later with the
FCALL command.

java
redis.functionLoad("FUNCTION LOAD "#!lua name=mylib\nredis.register_function('knockknock', function() return 'Who\\'s there?' end)");

String response = redis.fcall("knockknock", STATUS);

Using Functions is straightforward. Consuming results in Java requires
additional details to consume the result through a matching type. As we
do not know what your function will return, the API uses call-site
generics for you to specify the result type. Additionally, you must
provide a ScriptOutputType hint to EVAL so that the driver uses the
appropriate output parser. See Output Formats for
further details.

Output Formats

You can choose from one of the following:

  • BOOLEAN: Boolean output, expects a number 0 or 1 to be converted
    to a boolean value.

  • INTEGER: 64-bit Integer output, represented as Java Long.

  • MULTI: List of flat arrays.

  • STATUS: Simple status value such as OK. The Redis response is
    parsed as ASCII.

  • VALUE: Value return type decoded through RedisCodec.

  • OBJECT: RESP3-defined object output supporting all Redis response
    structures.

Leveraging Scripting and Functions through Command Interfaces

Using dynamic functionality without a documented response structure can
impose quite some complexity on your application. If you consider using
scripting or functions, then you can use Command
Interfaces
to declare
an interface along with methods that represent your scripting or
function landscape. Declaring a method with input arguments and a
response type not only makes it obvious how the script or function is
supposed to be called, but also how the response structure should look
like.

Letโ€™s take a look at a simple function call first:

lua
local function my_hlastmodified(keys, args)
  local hash = keys[1]
  return redis.call('HGET', hash, '_last_modified_')
end
java
Long lastModified = redis.fcall("my_hlastmodified", INTEGER, "my_hash");

This example calls the my_hlastmodified function expecting some Long
response an input argument. Calling a function from a single place in
your code isnโ€™t an issue on its own. The arrangement becomes problematic
once the number of functions grows or you start calling the functions
with different arguments from various places in your code. Without the
function code, it becomes impossible to investigate how the response
mechanics work or determine the argument semantics, as there is no
single place to document the function behavior.

Letโ€™s apply the Command Interface pattern to see how the the declaration
and call sites change:

java
interface MyCustomCommands extends Commands {

    /**
     * Retrieve the last modified value from the hash key.
     * @param hashKey the key of the hash.
     * @return the last modified timestamp, can be {@code null}.
     */
    @Command("FCALL my_hlastmodified 1 :hashKey")
    Long getLastModified(@Param("my_hash") String hashKey);

}

MyCustomCommands myCommands = โ€ฆ;
Long lastModified = myCommands.getLastModified("my_hash");

By declaring a command method, you create a place that allows for
storing additional documentation. The method declaration makes clear
what the function call expects and what you get in return.


File: docs/user-guide/redis-json.md

RedisJSON support in Lettuce

Lettuce supports RedisJSON starting from Lettuce 6.5.0.RELEASE.

The driver generally allows three distinct ways of working with the RedisJSON module:

  • (Default mode) - default JSON parsing using Jackson behind the scenes
  • (Advanced mode) - custom JSON parsing using a user-provided JSON parser
  • (Power-user mode) - unprocessed JSON documents that have not gone through any process of deserialization or serialization

!!! INFO
In all the above modes, the driver would refrain from processing the JSON document in the main event loop and instead
delegate this to the user thread. This behaviour is consistent when both receiving and sending JSON documents - when
receiving the parsing is done lazily whenever a method is called that requires the JSON to be parsed; when sending the
JSON is serialized immediately after it is passed to any of the commands, but before dispatching the command to the
event loop.

!!! WARNING
Unless you are using a custom JSON parser you would need to add a dependency to the
jackson-databind library in your project. This is because the
default JSON parser uses Jackson to parse JSON documents to and from string representations.

Default mode

Best for:

  • Most typical use-cases where the JSON document is parsed and processed

Example usage

java
RedisURI redisURI = RedisURI.Builder.redis("acme.com").build();
RedisClient redisClient = RedisClient.create(redisURI);
try (StatefulRedisConnection connect = redisClient.connect()){
    redis = connect.async();
    JsonPath path = JsonPath.of("$..mountain_bikes[0:2].model");

    JsonParser parser = redis.getJsonParser();
    JsonObject bikeRecord = parser.createJsonObject();
    JsonObject bikeSpecs = parser.createJsonObject();
    JsonArray bikeColors = parser.createJsonArray();

    bikeSpecs.put("material", parser.createJsonValue("\"wood\""));
    bikeSpecs.put("weight", parser.createJsonValue("19"));
    
    bikeColors.add(parser.createJsonValue("\"walnut\""));
    bikeColors.add(parser.createJsonValue("\"chestnut\""));
    
    bikeRecord.put("id", parser.createJsonValue("\"bike:13\""));
    bikeRecord.put("model", parser.createJsonValue("\"Woody\""));
    bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\""));
    bikeRecord.put("price", parser.createJsonValue("\"1112\""));
    bikeRecord.put("specs", bikeSpecs);
    bikeRecord.put("colors", bikeColors);
    
    String result = redis.jsonSet("bikes:inventory", path, bikeRecord).get();
}

Advanced mode

Best for:

  • Applications that want to handle parsing manually - either by using another library or by implementing their own parser

Example usage

java
RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();

try (RedisClient client = RedisClient.create(redisURI)) {
    client.setOptions(ClientOptions.builder().jsonParser(Mono.just(new CustomParser())).build());
    StatefulRedisConnection<String, String> connection = client.connect(StringCodec.UTF8);
    RedisCommands<String, String> redis = connection.sync();
}

Power-user mode

Best for:

  • Applications that do little to no processing on the Java layer

Example usage

java
JsonPath myPath = JsonPath.of("$..mountain_bikes");
RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();
try (RedisClient client = RedisClient.create(redisURI)) {
    RedisAsyncCommands<String, String> redis = client.connect().async();
    RedisFuture<List<JsonValue>> bikes = redis.jsonGet("bikes:inventory", myPath);

    CompletionStage<RedisFuture<String>> stage = bikes.thenApply(
            fetchedBikes -> redis.jsonSet("service_bikes", JsonPath.ROOT_PATH, fetchedBikes.get(0)));

    String result = stage.toCompletableFuture().get().get();
}

!!! NOTE
The power-user mode is not exclusive to using a custom parser (Advanced mode), as long as the custom parser follows
the API contract of the JsonParser, JsonValue, JsonArray and JsonObject interfaces.


File: docs/user-guide/transactions-multi.md

Transactions/Multi

Transactions allow the execution of a group of commands in a single
step. Transactions can be controlled using WATCH, UNWATCH, EXEC,
MULTI and DISCARD commands. Synchronous, asynchronous, and reactive
APIs allow the use of transactions.

!!! note
Transactional use requires external synchronization when a single
connection is used by multiple threads/processes. This can be achieved
either by serializing transactions or by providing a dedicated
connection to each concurrent process. Lettuce itself does not
synchronize transactional/non-transactional invocations regardless of
the used API facade.

Redis responds to commands invoked during a transaction with a QUEUED
response. The response related to the execution of the command is
received at the moment the EXEC command is processed, and the
transaction is executed. The particular APIs behave in different ways:

  • Synchronous: Invocations to the commands return null while they are
    invoked within a transaction. The MULTI command carries the response
    of the particular commands.

  • Asynchronous: The futures receive their response at the moment the
    EXEC command is processed. This happens while the EXEC response is
    received.

  • Reactive: An Obvervable<T> triggers onNext/onCompleted at the
    moment the EXEC command is processed. This happens while the EXEC
    response is received.

As soon as youโ€™re within a transaction, you wonโ€™t receive any responses
on triggering the commands

java
redis.multi() == "OK"
redis.set(key, value) == null
redis.exec() == list("OK")

Youโ€™ll receive the transactional response when calling exec() on the
end of your transaction.

java
redis.multi() == "OK"
redis.set(key1, value) == null
redis.set(key2, value) == null
redis.exec() == list("OK", "OK")

Transactions using the asynchronous API

Asynchronous use of Redis transactions is very similar to
non-transactional use. The asynchronous API returns RedisFuture
instances that eventually complete and they are handles to a future
result. Regular commands complete as soon as Redis sends a response.
Transactional commands complete as soon as the EXEC result is
received.

Each command is completed individually with its own result so users of
RedisFuture will see no difference between transactional and
non-transactional RedisFuture completion. That said, transactional
command results are available twice: Once via RedisFuture of the
command and once through List<Object> (TransactionResult since
Lettuce 5) of the EXEC command future.

java
RedisAsyncCommands<String, String> async = client.connect().async();

RedisFuture<String> multi = async.multi();

RedisFuture<String> set = async.set("key", "value");

RedisFuture<List<Object>> exec = async.exec();

List<Object> objects = exec.get();
String setResult = set.get();

objects.get(0) == setResult

Transactions using the reactive API

The reactive API can be used to execute multiple commands in a single
step. The nature of the reactive API encourages nesting of commands. It
is essential to understand the time at which an Observable<T> emits a
value when working with transactions. Redis responds with QUEUED to
commands invoked during a transaction. The response related to the
execution of the command is received at the moment the EXEC command is
processed, and the transaction is executed. Subsequent calls in the
processing chain are executed after the transactional end. The following
code starts a transaction, executes two commands within the transaction
and finally executes the transaction.

java
RedisReactiveCommands<String, String> reactive = client.connect().reactive();
reactive.multi().subscribe(multiResponse -> {
    reactive.set("key", "1").subscribe();
    reactive.incr("key").subscribe();
    reactive.exec().subscribe();
});

Transactions on clustered connections

Clustered connections perform a routing by default. This means, that you
canโ€™t be really sure, on which host your command is executed. So if you
are working in a clustered environment, use rather a regular connection
to your node, since then youโ€™ll bound to that node knowing which hash
slots are handled by it.

Examples

Multi with executing multiple commands

java
redis.multi();

redis.set("one", "1");
redis.set("two", "2");
redis.mget("one", "two");
redis.llen(key);

redis.exec(); // result: list("OK", "OK", list("1", "2"), 0L)

Mult executing multiple asynchronous commands

java
redis.multi();

RedisFuture<String> set1 = redis.set("one", "1");
RedisFuture<String> set2 = redis.set("two", "2");
RedisFuture<String> mget = redis.mget("one", "two");
RedisFuture<Long> llen = mgetredis.llen(key);


set1.thenAccept(value -> โ€ฆ); // OK
set2.thenAccept(value -> โ€ฆ); // OK

RedisFuture<List<Object>> exec = redis.exec(); // result: list("OK", "OK", list("1", "2"), 0L)

mget.get(); // list("1", "2")
llen.thenAccept(value -> โ€ฆ); // 0L

Using WATCH

java
redis.watch(key);

RedisConnection<String, String> redis2 = client.connect();
redis2.set(key, value + "X");
redis2.close();

redis.multi();
redis.append(key, "foo");
redis.exec(); // result is an empty list because of the changed key

Scripting and Functions

Redis functionality can be extended through many ways, of which Lua
Scripting
and
Functions are two approaches
that do not require specific pre-requisites on the server.

2. Official Technical Reference & Guides (redis/docs)

Redis Docs

Note: if you are an AI agent, please see AI_AGENT_DEVELOPER_GUIDE.md for more information.

PRs are merged to the main branch of this repo. Merging to main automatically builds and publishes your changes to the live docs site (under /latest) โ€” there is no separate manual publish step, so allow a little time for the build and CDN cache to refresh after your PR is merged.
Before your PR is merged, you can preview your changes on a per-branch staging URL; the docs team's bot posts the staging links as a comment on your PR.

Site template files and folders

  • /archetypes: A Markdown file needs to have some front matter. An archetype defines which front matter is used when using hugo new content. Right now, the only supported archetype is the default one. Note: We might want to add additional archetypes in the future because most of our pages contain additional meta data properties like linkTitle.
  • /content: This folder contains the markdown files. We will have the subfolders like /develop, /integrate, and /operate
  • /assets: CSS files, site-wide icons, and images.
  • /data: Data files that are accessed by Hugo and rendered with the help of short codes or partials.
  • /layouts/partials: HTML templates that are used across sites. Examples are TOCs, breadcrumbs, or headers.
  • /layouts/$type: Each page type has at least the following templates to implement single.html and list.html. The single template is used to render a discrete page. The list template is used to render a collection of related pages (e.g., all sub-pages).
  • /layouts/home.html: The home page of the site, that is, the page that is displayed when you open the root path.
  • /layouts/404.html: The default 404 page.
  • /layouts/shortcodes HTML template snippets that you want to leverage within Markdown files. Examples are tabbed code examples or notification blocks.
  • /public: This is the folder that is generated by Hugo. This content needs to be published by site maintainers.
  • /static: Any static files that need to be accessed by the site, e.g., CSS or JavaScript.
  • /package.json: Node.js dependencies. Tailwind, for example, is installed via the Node package manager (npm).
  • /config.toml: Hugo's site configuration, like the root path and menu items. Hugo can access configuration elements when rendering the site. So you can define custom configuration settings here.
  • /Makefile: We use make to wrap some Hugo commands and to add additional build steps.
  • /tailwind.config.js: This is the Tailwind CSS framwork's configuration file.
  • /postcss.config.js: Needed to make Tailwind statically accessible to the site.

Requirements

You will need the following tools to build the site locally:

  • python3: Python >= 3.8.
  • node and npm: Node.js >= 19.7.0, and the Node.js package manager >= 9.5.0.
  • hugo: Hugo site generator, v0.143.0; the extended edition.
  • make: To run the makefile.
  • git: To manage the source code of this repo.

Build script and data files

The site can be built using the command make all. Here is what's executed:

  1. Clean the current folder by deleting the generated output of a previous build.
  2. Install the necessary Node.js and Python dependencies.
  3. Build the components that were fetched from external Github repositories (e.g., client examples). This fetches source code files and generates data files.
  4. Execute the hugo command to generate the HTML pages. The build result can be found within the folder /public.

The command make serve performs the same steps, but serves the web pages on the local host for development purposes.

The Makefile contains details about the executed commands.

N.B. once you've built the site once, you can continuing working without rebuilding every time you make a change.
Use hugo serve to serve the docs locally.
The only time you really need to rebuild the site locally is when new code samples need to be pulled from the client sites.

Build pipeline

The build pipeline that is defined within .github/workflows/main.yml builds the documentation and uploads it into a GCS bucket:

  1. Install Hugo
  2. Check the branch out
  3. Perform the build as stated before by using make all
  4. Install the Google Cloud CLI
  5. Authenticate to the GCS bucket
  6. Validate the branch name
  7. Sync the branch with a GCS folder

Hugo specifics

We are using the following syntax for Hugo relrefs:

text
[Link title]({{< relref "link relative to the site's base url" >}})

Here is an example:

text
[Data structure store]({{< relref "/develop/get-started/data-store" >}})

It's strongly advised to use relref because it provides the following advantages:

  1. Links are checked at build time. Any broken references within the site are reported, stopping the build.
  2. References are prefixed with the site's base URL, which means that they work in builds with a different base URL.

The following needs to be taken into account when using relref: The reference /develop/get-started/data-store and /develop/get-started/data-store/ aren't the same. You must use the trailing slash if the referenced article is an _index.md file within a folder (e.g., .../data-store/ for .../data-store/_index.md). Otherwise, you should not use the trailing slash (e.g., .../get-started/data-store.md).

Images

The image shortcode doesn't need to be closed anymore. Here is an example;

text
{{< image filename="/images/rc/icon-database-update-status-pending.png" alt="Database update" >}}

The filename property value can be any file name path which is relative to the site's base path.

We added a new property class which allows you to override the CSS class of the image. Images have by default a block display in Tailwind. You can change this by setting the class property to inline. The following example shows two images that are in a single line:

text
{{< image filename="/images/rc/icon-database-update-status-pending.png#no-click" alt="Pending database status" class="inline" >}} &nbsp; {{< image filename="/images/rc/icon-database-status-active.png#no-click" alt="Active database status" class="inline" >}}

Templating

Variables are scoped in the context of their block. Overriding a variable within an if block doesn't change the variable value as soon as you leave that block. This is a specific limitation of the Go templating language.

The following will give you the / outside of the condition block if the path was initially set to /:

text
{{ $path := $parsed.Path }}
{{ if (eq $path "/") }}
	{{ $path = "" }}
{{ end }}

{{- $path -}}

The solution is to use a scratchpad for storing the 'global' state:

text
{{ $path := $parsed.Path }}
{{ if (eq $path "/") }}
	{{ .Scratch.Set "path" ""}}
{{ else }}
    {{ .Scratch.Set "path" $path}}
{{ end }}

{{- .Scratch.Get "path" -}}

Child pages

The redis.io template adds a list of links to child pages at the bottom of each section page. You can prevent this by adding the following front matter property:

text
hideListLinks: true

Tailwind specifics

Fonts

The fonts are located in the folder static/fonts. This folder contains typically file of the formats ttf, woff, or woff2. You can perform the following steps to add a font:

  • Copy your font files to the fonts folder static/fonts.

  • Ensure that the font is loaded by adding it to the partial layouts/partials/fonts.html. This partial defines the font using a style sheet and ensures that the font resources are pre-loaded.

    Here is an example that shows how to define the font face 'Geist Mono' with a regular font weight:

    text
    @font-face {
        font-family: 'Geist Mono';
        src: url('{{ $basePath }}/fonts/GeistMono-Regular.woff2') format('woff2');
        font-weight: 400;
        font-style: normal;
        font-display: swap;
    }
  • Make your font available via Tailwind by adding it to the tailwind.config.js file.

    text
    module.exports = {
      content: ["content/**/*.md", "layouts/**/*.html", "./tailwindcss.whitelist.txt"],
      theme: {
        extend: {
          fontFamily: {
                                    sans: [ 'Space Grotesk', ...defaultTheme.fontFamily.sans ],
                                    mono: [ 'Space Mono', 'SF Mono', ...defaultTheme.fontFamily.mono ],
                                    monogeist: ['Geist Mono', ...defaultTheme.fontFamily.mono ],
          },
          ...
  • Use a font-* CSS class within HTML or your style sheets, whereby the * needs to be substituted with one of the Tailwind fonts that you defined in the Tailwind configuration file (tailwind.config.js).

    Here is an example how to use font-monogeist in the global CSS file assets/css/index.css to change the font of code blocks:

    text
    .prose pre > code {
      @apply bg-none font-monogeist;
    }

Troubleshooting

NIL pointer error when rendering a section

You might sometimes see the following error message:

text
nil pointer evaluating page.Page.Params

This happened for us when Hugo executed the bannerText logic, which inspected all parent pages by checking if they have a banner text set. The problem was that one of the parent folders was missing an _index.md file, which meant the folder couldn't be identified as a page. The solution was to add an _index.md file to the folder that didn't have it yet.