Redis — это хранилище данных, поддерживающее более 190 документированных команд и более 450 командных перестановок. Сообщество активно поддерживает развитие Redis; каждый крупный релиз Redis поставляется с новыми командами. В этом году Redis был открыт для сторонних поставщиков для разработки модулей, расширяющих функциональность Redis. Командный рост и отслеживание будущих модулей — сложная задача для разработчиков клиентов и пользователей Redis.
Командный рост
Командный рост в Redis — сложный бизнес для клиентских библиотек. Несколько клиентов предоставляют типизированный API, который объявляет сигнатуру метода (функции) для каждого вызова API Redis. Статические объявления полезны для использования, но количество команд Redis загрязняет клиентов множеством сигнатур методов. Некоторые команды могут выполняться по-разному, что влияет на тип ответа ( ZREVRANGE
, ZREVRANGE … WITHSCORES
), для которого требуются дополнительные подписи. Давайте подробнее рассмотрим некоторые сигнатуры методов:
Redis-гь
1
2
3
4
5
|
# Get the values of all the given hash fields. # # @example # redis.hmget( "hash" , "f1" , "f2" ) def hmget(key, *fields, &blk) |
Jedis
1
|
public List<String> hmget( final String key, final String... fields) |
салат
1
2
|
List<V> public List<K> hmget(K key, K... fields) |
Объявленные методы обеспечивают безопасность типов и документацию для разработчиков, но в то же время они статичны. Как только Redis вводит новую команду, поставщик клиента должен изменить API, иначе новые команды не будут использоваться. Большинство клиентов Redis предоставляют клиентский API для выполнения пользовательских команд для решения этой проблемы:
Redis-гь
1
|
client.call([:hmget, key] + fields) |
Jedis
1
2
|
final byte [][] params = …; jedis.sendCommand(HMGET, params); |
салат
1
2
3
4
|
lettuce.dispatch(CommandType.HMGET, new ValueListOutput<>(codec), new CommandArgs<>(codec) .addKey(key) .addKey(field)); |
Jedipus
1
|
rce.accept(client -> client.sendCmd(Cmds.HMGET, "hash" , "field1" , "field2" , …)); |
Другие клиенты, такие как node_redis
создают прототипы функций на основе команд Redis. Это улучшение статических API, поскольку оно обеспечивает определенную гибкость в API.
Создание команды Redis требует знаний о ее запросах и структуре ответов. Это знание записывается в месте внутри вызывающего кода. Это удобно, потому что вы помещаете его туда, где вам нужен код, но у него есть несколько недостатков. Поскольку пользовательские команды запускаются из метода, пользовательские команды требуют дополнительных усилий для повторного использования. Типичная сигнатура метода, найденная на многих клиентах, не требуется. Этот подход делает интроспекцию более сложной, если не следовать подходу компонента API. Это потому, что все пользовательские команды вызывают один и тот же метод только с разными аргументами.
Характер объявлений статического метода с фиксированным списком параметров ограничен, чтобы принимать только предоставленные параметры. Контекстные элементы управления вызовами методов не могут быть применены через этот метод. Например, салат предоставляет синхронный API, который позволяет контролировать время ожидания команд для всех команд, но не на уровне вызова команд.
Давайте подойдем к Redis с динамическим API.
Динамический API
Динамические API — это программные интерфейсы, которые дают определенную гибкость, поскольку они следуют соглашениям. Динамические API могут быть известны из клиентских прокси Resteasy или из запроса данных Spring Data . Оба являются интерфейсами, которые живут в пользовательском коде. Resteasy / Spring Data проверяют интерфейсы и реализуют их, предоставляя прокси Java. Вызовы методов на этих интерфейсах (прокси) перехватываются, проверяются и преобразуются в соответствующий вызов. Давайте посмотрим, как это может работать для Java и Redis:
Простой командный интерфейс
1
2
3
4
5
|
public interface MyRedisCommands { List<String> hmget(String key, String... values); } |
Интерфейс сверху объявляет один метод: List<String > hmget(String key, String... fields)
. Мы можем извлечь из этой декларации определенные вещи:
- Он должен выполняться синхронно — в типе результата не объявлена асинхронная или реактивная оболочка
- Командный метод Redis возвращает
List
ofString
s — который сообщает нам об ожидаемом результате команды, поэтому мы ожидаем массив Redis и преобразуем каждый элемент в строку - Метод называется
hmget
. Поскольку это единственная доступная деталь, мы предполагаем, что команда называетсяhmget
. - Определены два параметра:
String key
иString... values
. Это говорит нам о порядке параметров и их типах. Хотя Redis не принимает никакие другие типы параметров, кроме объемных строк, мы все же можем применить преобразование к параметрам — мы можем завершить их сериализацию из объявленного типа.
Вышеуказанная команда будет выглядеть так:
1
|
commands.hmget( "key" , "field1" , "field2" ); |
и переведен в команду Redis:
1
|
HMGET key field1 field2 |
Объявление в интерфейсе имеет два интересных свойства:
- Есть подпись метода. Хотя это очевидный факт, это обычный исполняемый файл, который вызывается. Это позволяет быстро анализировать вызывающие абоненты, создавая поиск ссылок на этот метод.
- Над подписью метода есть пустое место, идеально для документации.
Несколько моделей исполнения
1
2
3
4
5
6
7
8
9
|
public interface MyRedisCommands { List<String> hmget(Timeout timeout, String key, String... values); RedisFuture<List<String>> mget(String... keys); Flux<String> smembers(String key); } |
Динамический API допускает отклонения в типах возвращаемых данных. Давайте посмотрим, как это влияет на вещи, которые мы можем извлечь из их типов возвращаемых данных.
- Вы уже знаете, что
hmget
выполняется блокирующим образом. Но подождите, что это за параметрTimeout
? Это собственный тип параметра для объявления тайм-аута на уровне вызова. Базовое выполнение применяет тайм-ауты от параметра, а не значения по умолчанию, установленные на уровне соединения. -
mget
объявляет возвращаемый типRedisFuture
себеList
String
.RedisFuture
является типом оболочки для асинхронного выполнения и возвращает дескриптор для выполнения синхронизации или объединения методов на более позднем этапе. Этот метод может быть выполнен асинхронно. -
smembers
используетFlux
ofString
. Исходя из типа возвращаемого значения, мы можем ожидать два свойства:Flux
— это оболочка реактивного выполнения, которая задерживает выполнение до тех пор, пока подписчик не подпишется наFlux
. ТипList
исчез, потому чтоFlux
может испускать0..N
элементов, поэтому мы можем принять решение о потоковом реактивном выполнении.
Структура команды
01
02
03
04
05
06
07
08
09
10
11
12
13
|
public interface MyRedisCommands { List<String> mget(String... keys); @Command ( "MGET" ) RedisFuture<List<String>> mgetAsync(String... keys); @CommandNaming (strategy = DOT) double nrRun(String key, int ... indexes) @Command ( "NR.OBSERVE ?0 ?1 -> ?2 TRAIN" ) List<Integer> nrObserve(String key, int [] in, int ... out) } |
Java требует, чтобы методы различались по имени или типу параметра. Дисперсия только в типе возврата поддерживается на уровне байт-кода, но не при написании методов в вашем коде. Что если вы хотите объявить один синхронно выполняемый метод и один асинхронно выполняемый, принимающий одинаковые параметры? Вам нужно указать другое имя. Но не противоречит ли это ранее объясненному происхождению имени? Оно делает.
- Присмотритесь к
mget
иmgetAsync
. Оба метода предназначены для выполнения командыMGET
— синхронно и асинхронно.mgetAsync
имеет аннотацию@Command
которая предоставляет имя команды команде и отменяет предположение, что в противном случае метод будет называтьсяMGETASYNC
. - Redis открыт для модулей. Каждый модуль может расширять Redis, предоставляя новые команды, где шаблон команд следует руководству <PREFIX>. <COMMAND>. Однако точки не допускаются в именах методов Java. Давайте применим другую стратегию именования к
nrRun
с@CommandNaming(strategy = DOT)
. Горб верблюдов (изменения в буквенном регистре) выражается путем размещения точки между отдельными сегментами команд, и мы можем запуститьNR.RUN
из Neural Redis . - Некоторые команды имеют более сложный синтаксис, который не позволяет просто объединять параметры. Посмотрите на
NR.OBSERVE
. Он состоит из трех статических частей с параметрами между ними. Эта структура команд выражается на языке, похожем на команду.NR.OBSERVE ?0 ?1 -> ?2 TRAIN
описывает команду как строку и вставляет ссылки на индексы для аргументов. Все строковые части в команде являются константами, а ссылки на параметры заменяются фактическими параметрами.
Вывод
Применение динамического API к Redis смещает представление в новую перспективу. Он может предоставить пользователям упрощенный пользовательский подход к командам, не жертвуя возможностью повторного использования. Природа объявления метода создает место для документирования и самоанализа относительно его вызывающих.
Динамический API полезен также для других приложений, использующих RESP, таких как Disque или Tile38 .
Экспериментальная реализация доступна с салатом из репозитория Sonatype OSS Snapshot https://oss.sonatype.org/content/repositories/snapshots/ :
1
2
3
4
5
|
<dependency> <groupId>biz.paluch.redis</groupId> <artifactId>lettuce</artifactId> <version> 5.0 . 0 -dynamic-api-SNAPSHOT</version> </dependency> |
Использование RedisCommandFactory
01
02
03
04
05
06
07
08
09
10
11
12
|
RedisCommandFactory factory = new RedisCommandFactory(connection); TestInterface api = factory.getCommands(TestInterface. class ); String value = api.get( "key" ); public interface TestInterface { String get(String key); @Command ( "GET" ) byte [] getAsBytes(String key); } |
Ссылка
-
@Command
: аннотация команды с указанием имени команды или всей структуры команды с использованием языка, подобного команде. -
@CommandNaming
: аннотация для указания стратегии именования команд. -
Timeout
: объект значения, содержащий тайм-аут. -
RedisFuture
: дескриптор будущего. -
Flux
: издатель Project Reactor для реактивного выполнения, который испускает0..N
элементов.
Ссылка: | Командные интерфейсы: подход Redis с динамическими API в Java от нашего партнера по JCG Марка Палуча в блоге paluch.biz . |