Перейти к основному содержимому

Введение в Lettuce — клиент Java Redis

· 11 мин. чтения

1. Обзор

Эта статья представляет собой введение в Lettuce , Java-клиент Redis .

Redis — это хранилище ключей и значений в памяти, которое можно использовать в качестве базы данных, кэша или брокера сообщений. Данные добавляются, запрашиваются, изменяются и удаляются с помощью команд , которые работают с ключами в структуре данных Redis в памяти.

Lettuce поддерживает как синхронную, так и асинхронную связь с использованием полного API Redis, включая его структуры данных, обмен сообщениями pub/sub и высокодоступные подключения к серверу.

2. Почему салат?

Мы рассказали о джедаях в одном из предыдущих постов. Что отличает салат?

Наиболее существенным отличием является его асинхронная поддержка через интерфейс CompletionStage Java 8 и поддержка Reactive Streams. Как мы увидим ниже, Lettuce предлагает естественный интерфейс для выполнения асинхронных запросов к серверу базы данных Redis и для создания потоков.

Он также использует Netty для связи с сервером. Это делает API «тяжелее», но также делает его более подходящим для совместного использования соединения более чем одним потоком.

3. Настройка

3.1. Зависимость

Давайте начнем с объявления единственной зависимости, которая нам понадобится в pom.xml :

<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.0.1.RELEASE</version>
</dependency>

Последнюю версию библиотеки можно проверить в репозитории Github или на Maven Central.

3.2. Установка Redis

Нам нужно будет установить и запустить как минимум один экземпляр Redis, два, если мы хотим протестировать кластеризацию или дозорный режим (хотя для дозорного режима требуется три сервера для правильной работы). В этой статье мы используем 4.0.x — последняя стабильная версия на данный момент.

Дополнительную информацию о начале работы с Redis можно найти здесь , включая загрузки для Linux и MacOS.

Redis официально не поддерживает Windows, но здесь есть порт сервера . Мы также можем запустить Redis в Docker , что является лучшей альтернативой для Windows 10 и быстрым способом приступить к работе.

4. Соединения

4.1. Подключение к серверу

Подключение к Redis состоит из четырех шагов:

  1. Создание URI Redis
  2. Использование URI для подключения к RedisClient
  3. Открытие соединения Redis
  4. Генерация набора RedisCommands

Посмотрим на реализацию:

RedisClient redisClient = RedisClient
.create("redis://password@localhost:6379/");
StatefulRedisConnection<String, String> connection
= redisClient.connect();

StatefulRedisConnection — это то, на что это похоже; потокобезопасное соединение с сервером Redis, которое будет поддерживать соединение с сервером и переподключаться при необходимости. Когда у нас есть соединение, мы можем использовать его для выполнения команд Redis синхронно или асинхронно.

RedisClient использует значительные системные ресурсы, поскольку содержит ресурсы Netty для связи с сервером Redis. Приложения, требующие нескольких подключений, должны использовать один RedisClient.

4.2. Redis URI

Мы создаем RedisClient , передавая URI методу статической фабрики.

Lettuce использует собственный синтаксис для URI Redis. Это схема:

redis :// [password@] host [: port] [/ database]
[? [timeout=timeout[d|h|m|s|ms|us|ns]]
[&_database=database_]]

Существует четыре схемы URI:

  • redis — автономный сервер Redis
  • rediss — автономный сервер Redis через SSL-соединение.
  • redis-socket — автономный сервер Redis через сокет домена Unix.
  • redis-sentinel — сервер Redis Sentinel

Экземпляр базы данных Redis можно указать как часть URL-адреса или как дополнительный параметр. Если указаны оба, параметр имеет более высокий приоритет.

В приведенном выше примере мы используем строковое представление. В Lettuce также есть класс RedisURI для создания соединений. Он предлагает шаблон Builder :

RedisURI.Builder
.redis("localhost", 6379).auth("password")
.database(1).build();

И конструктор:

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

4.3. Синхронные команды

Подобно Jedis, Lettuce предоставляет полный набор команд Redis в виде методов.

Однако Lettuce реализует как синхронную, так и асинхронную версии. Мы кратко рассмотрим синхронную версию, а затем будем использовать асинхронную реализацию в остальной части руководства.

После того, как мы создадим соединение, мы используем его для создания набора команд:

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

Теперь у нас есть интуитивно понятный интерфейс для общения с Redis.

Мы можем установить и получить строковые значения:

syncCommands.set("key", "Hello, Redis!");

String value = syncommands.get(“key”);

Мы можем работать с хешами:

syncCommands.hset("recordName", "FirstName", "John");
syncCommands.hset("recordName", "LastName", "Smith");
Map<String, String> record = syncCommands.hgetall("recordName");

Мы рассмотрим больше Redis позже в этой статье.

Синхронный API Lettuce использует асинхронный API. Блокировка делается для нас на командном уровне. Это означает, что несколько клиентов могут совместно использовать синхронное соединение.

4.4. Асинхронные команды

Давайте посмотрим на асинхронные команды:

RedisAsyncCommands<String, String> asyncCommands = connection.async();

Мы извлекаем набор RedisAsyncCommands из соединения аналогично тому, как мы извлекали синхронный набор. Эти команды возвращают RedisFuture ( внутренне CompletableFuture ) :

RedisFuture<String> result = asyncCommands.get("key");

Руководство по работе с CompletableFuture можно найти здесь.

4.5. Реактивный API

Наконец, давайте посмотрим, как работать с неблокирующим реактивным API:

RedisStringReactiveCommands<String, String> reactiveCommands = connection.reactive();

Эти команды возвращают результаты, завернутые в Mono или Flux из Project Reactor .

Руководство по работе с Project Reactor можно найти здесь.

5. Структуры данных Redis

Мы кратко рассмотрели строки и хэши выше, давайте посмотрим, как Lettuce реализует остальные структуры данных Redis. Как и следовало ожидать, каждая команда Redis имеет одноименный метод.

5.1. Списки

Списки — это списки строк с сохраненным порядком вставки. Значения вставляются или извлекаются с любого конца:

asyncCommands.lpush("tasks", "firstTask");
asyncCommands.lpush("tasks", "secondTask");
RedisFuture<String> redisFuture = asyncCommands.rpop("tasks");

String nextTask = redisFuture.get();

В этом примере nextTask равно « firstTask ». Lpush помещает значения в начало списка, а затем rpop извлекает значения из конца списка.

Мы также можем извлекать элементы с другого конца:

asyncCommands.del("tasks");
asyncCommands.lpush("tasks", "firstTask");
asyncCommands.lpush("tasks", "secondTask");
redisFuture = asyncCommands.lpop("tasks");

String nextTask = redisFuture.get();

Начнем второй пример с удаления списка с помощью del . Затем мы снова вставляем те же значения, но используем lpop для извлечения значений из начала списка, поэтому nextTask содержит текст « secondTask ».

5.2. Наборы

Наборы Redis — это неупорядоченные наборы строк , похожие на наборы Java ; нет повторяющихся элементов:

asyncCommands.sadd("pets", "dog");
asyncCommands.sadd("pets", "cat");
asyncCommands.sadd("pets", "cat");

RedisFuture<Set<String>> pets = asyncCommands.smembers("nicknames");
RedisFuture<Boolean> exists = asyncCommands.sismember("pets", "dog");

Когда мы извлекаем набор Redis как Set , размер равен двум, поскольку дубликат «кошки» был проигнорирован. Когда мы запрашиваем у Redis наличие «собаки» с sismember, ответ будет верным.

5.3. Хэши

Ранее мы кратко рассмотрели пример хэшей. Они заслуживают быстрого объяснения.

Хэши Redis — это записи со строковыми полями и значениями. Каждая запись также имеет ключ в первичном индексе:

asyncCommands.hset("recordName", "FirstName", "John");
asyncCommands.hset("recordName", "LastName", "Smith");

RedisFuture<String> lastName
= syncCommands.hget("recordName", "LastName");
RedisFuture<Map<String, String>> record
= syncCommands.hgetall("recordName");

Мы используем hset для добавления полей в хеш, передавая имя хэша, имя поля и значение.

Затем мы получаем отдельное значение с помощью hget, имя записи и поле. Наконец, мы получаем всю запись в виде хэша с помощью hgetall.

5.4. Сортированные наборы

Сортированные наборы содержат значения и ранг, по которому они сортируются. Ранг представляет собой 64-битное значение с плавающей запятой.

Элементы добавляются с рангом и извлекаются в диапазоне:

asyncCommands.zadd("sortedset", 1, "one");
asyncCommands.zadd("sortedset", 4, "zero");
asyncCommands.zadd("sortedset", 2, "two");

RedisFuture<List<String>> valuesForward = asyncCommands.zrange(key, 0, 3);
RedisFuture<List<String>> valuesReverse = asyncCommands.zrevrange(key, 0, 3);

Второй аргумент zadd — ранг. Мы получаем диапазон по рангу с помощью zrange для возрастания и zrevrange для убывания.

Мы добавили « ноль » с рангом 4, поэтому он появится в конце valuesForward и в начале valuesReverse.

6. Транзакции

Транзакции позволяют выполнять набор команд за один атомарный шаг. Эти команды гарантированно выполняются по порядку и исключительно. Команды от другого пользователя не будут выполняться до завершения транзакции.

Либо выполняются все команды, либо ни одна из них. Redis не будет выполнять откат, если один из них выйдет из строя. После вызова exec() все команды выполняются в указанном порядке.

Давайте посмотрим на пример:

asyncCommands.multi();

RedisFuture<String> result1 = asyncCommands.set("key1", "value1");
RedisFuture<String> result2 = asyncCommands.set("key2", "value2");
RedisFuture<String> result3 = asyncCommands.set("key3", "value3");

RedisFuture<TransactionResult> execResult = asyncCommands.exec();

TransactionResult transactionResult = execResult.get();

String firstResult = transactionResult.get(0);
String secondResult = transactionResult.get(0);
String thirdResult = transactionResult.get(0);

Вызов multi запускает транзакцию. Когда транзакция запускается, последующие команды не выполняются до тех пор, пока не будет вызвана функция exec() .

В синхронном режиме команды возвращают null. В асинхронном режиме команды возвращают RedisFuture . Exec возвращает TransactionResult , который содержит список ответов.

Поскольку RedisFuture также получает свои результаты, клиенты асинхронного API получают результат транзакции в двух местах.

7. Пакетирование

В нормальных условиях Lettuce выполняет команды, как только они вызываются клиентом API.

Это то, что требуется большинству обычных приложений, особенно если они полагаются на последовательное получение результатов команд.

Однако такое поведение неэффективно, если приложениям не нужны результаты немедленно или если выполняется массовая загрузка больших объемов данных.

Асинхронные приложения могут переопределить это поведение:

commands.setAutoFlushCommands(false);

List<RedisFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
futures.add(commands.set("key-" + i, "value-" + i);
}
commands.flushCommands();

boolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS,
futures.toArray(new RedisFuture[0]));

Если для setAutoFlushCommands задано значение false , приложение должно вызывать flushCommands вручную. В этом примере мы поставили в очередь несколько команд set, а затем сбросили канал. AwaitAll ожидает завершения всех RedisFuture .

Это состояние устанавливается для каждого соединения и влияет на все потоки, использующие это соединение. Эта функция не применима к синхронным командам.

8. Опубликовать/подписаться

Redis предлагает простую систему обмена сообщениями публикации/подписки. Подписчики получают сообщения из каналов с помощью команды подписки . Сообщения не сохраняются; они доставляются пользователям только тогда, когда они подписаны на канал.

Redis использует систему pub/sub для уведомлений о наборе данных Redis, предоставляя клиентам возможность получать события об установке, удалении, истечении срока действия ключей и т. д.

См. документацию здесь для более подробной информации.

8.1. Подписчик

RedisPubSubListener получает сообщения публикации/ подписки . Этот интерфейс определяет несколько методов, но мы покажем здесь только метод получения сообщений:

public class Listener implements RedisPubSubListener<String, String> {

@Override
public void message(String channel, String message) {
log.debug("Got {} on channel {}", message, channel);
message = new String(s2);
}
}

Мы используем RedisClient для подключения канала публикации/подписки и установки слушателя:

StatefulRedisPubSubConnection<String, String> connection
= client.connectPubSub();
connection.addListener(new Listener())

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

Установив прослушиватель, мы получаем набор RedisPubSubAsyncCommands и подписываемся на канал.

8.2. Издатель

Публикация — это просто вопрос подключения канала Pub/Sub и получения команд:

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

RedisPubSubAsyncCommands<String, String> async
= connection.async();
async.publish("channel", "Hello, Redis!");

Для публикации требуется канал и сообщение.

8.3. Реактивные подписки

Lettuce также предлагает реактивный интерфейс для подписки на сообщения pub/sub:

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

RedisPubSubAsyncCommands<String, String> reactive = connection
.reactive();

reactive.observeChannels().subscribe(message -> {
log.debug("Got {} on channel {}", message, channel);
message = new String(s2);
});
reactive.subscribe("channel").subscribe();

Flux , возвращаемый observChannels , получает сообщения для всех каналов, но, поскольку это поток, его легко фильтровать.

9. Высокая доступность

Redis предлагает несколько вариантов высокой доступности и масштабируемости. Для полного понимания требуется знание конфигураций сервера Redis, но мы рассмотрим краткий обзор того, как Lettuce их поддерживает.

9.1. Мастер/Раб

Серверы Redis реплицируют себя в конфигурации master/slave. Главный сервер отправляет подчиненному поток команд, которые реплицируют главный кэш подчиненному. Redis не поддерживает двунаправленную репликацию, поэтому ведомые устройства доступны только для чтения.

Lettuce может подключаться к системам Master/Slave, запрашивать у них топологию, а затем выбирать ведомые устройства для операций чтения, что может повысить пропускную способность:

RedisClient redisClient = RedisClient.create();

StatefulRedisMasterSlaveConnection<String, String> connection
= MasterSlave.connect(redisClient,
new Utf8StringCodec(), RedisURI.create("redis://localhost"));

connection.setReadFrom(ReadFrom.SLAVE);

9.2. Страж

Redis Sentinel отслеживает основные и подчиненные экземпляры и организует отработку отказа на подчиненных в случае отказа главного.

Lettuce может подключиться к Sentinel, использовать его для обнаружения адреса текущего мастера, а затем вернуть соединение с ним.

Для этого создадим другой RedisURI и подключим к нему наш RedisClient :

RedisURI redisUri = RedisURI.Builder
.sentinel("sentinelhost1", "clustername")
.withSentinel("sentinelhost2").build();
RedisClient client = new RedisClient(redisUri);

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

Мы создали URI с именем хоста (или адресом) первого Sentinel и именем кластера, за которым следует второй адрес Sentinel. Когда мы подключаемся к Sentinel, Lettuce запрашивает его о топологии и возвращает нам соединение с текущим главным сервером.

Полная документация доступна здесь.

9.3. Кластеры

Redis Cluster использует распределенную конфигурацию для обеспечения высокой доступности и высокой пропускной способности.

Кластеризирует ключи сегментов до 1000 узлов, поэтому транзакции в кластере недоступны:

RedisURI redisUri = RedisURI.Builder.redis("localhost")
.withPassword("authentication").build();
RedisClusterClient clusterClient = RedisClusterClient
.create(rediUri);
StatefulRedisClusterConnection<String, String> connection
= clusterClient.connect();
RedisAdvancedClusterCommands<String, String> syncCommands = connection
.sync();

RedisAdvancedClusterCommands содержит набор команд Redis, поддерживаемых кластером, и перенаправляет их на экземпляр, содержащий ключ.

Полная спецификация доступна здесь .

10. Заключение

В этом руководстве мы рассмотрели, как использовать Lettuce для подключения и запроса сервера Redis из нашего приложения.

Lettuce поддерживает полный набор функций Redis, а также полностью потокобезопасный асинхронный интерфейс. Он также широко использует интерфейс CompletionStage Java 8, чтобы предоставить приложениям детальный контроль над тем, как они получают данные.

Образцы кода, как всегда, можно найти на GitHub .