未加星标

Command Interfaces: Approaching Redis with dynamic APIs in Java

字体大小 | |
[数据库(综合) 所属分类 数据库(综合) | 发布者 店小二05 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

Redis is a data store supporting over 190 documented commands and over 450 command permutations. The community supports actively Redis development; each major Redis release comes with new commands. This year Redis was opened up for 3rd party vendors to develop modules that extend Redis functionality. Command growth and keeping track with upcoming modules are challenging for client developers and Redis users.

Command growth

Command growth in Redis is a challenging business for client libraries. Several clients expose a typed API that declares a method (function) signature for each Redis API call. Static declarations are beneficial for use but the amount of Redis commands pollutes clients with tons of method signatures. Some commands can be executed in different ways that affects the response type ( ZREVRANGE , ZREVRANGE … WITHSCORES ) that require additional signatures. Let’s take a closer look on some method signatures:

redis-rb # Get the values of all the given hash fields.
#
# @example
# redis.hmget("hash", "f1", "f2")
def hmget(key, *fields, &blk) jedis public List<String> hmget(final String key, final String... fields) lettuce List<V>
public List<K> hmget(K key, K... fields)

Declared methods provide type safety and documentation to developers, but they are static at the same time. As soon as a Redis introduces a new command, the client vendor has to change the API otherwise new commands are not usable. Most Redis clients expose a client call API to execute custom commands to address this issue:

redis-rb client.call([:hmget, key] + fields) jedis final byte[][] params = …;
jedis.sendCommand(HMGET, params); lettuce lettuce.dispatch(CommandType.HMGET, new ValueListOutput<>(codec),
new CommandArgs<>(codec)
.addKey(key)
.addKey(field)); Jedipus rce.accept(client -> client.sendCmd(Cmds.HMGET, "hash", "field1", "field2", …));

Other clients, like node_redis create function prototypes based on Redis commands. This is an improvement to static APIs because it enables a certain flexibility in the API.

Constructing a Redis command requires knowledge about its request and response structure. This knowledge is written down at a location inside of the calling code. This is handy because you put it in the place where you need the code, but it comes with a few downsides. Because custom commands are run from inside a method, custom commands require additional effort to be reusable. The typical method signature as found on many clients is not required. This approach makes introspection more challenging, if not following an API component approach. This is, because all custom commands call the same method with just different arguments.

The nature of static method declarations with a fixed parameter list is limited to accept the provided parameters only. Contextual controls to method calls cannot be applied through that method. Lettuce for example provides a synchronous API that allows controlling the command timeout for all commands but not on command invocation level.

Let’s approach Redis with a dynamic API.

Dynamic API

Dynamic APIs are programming interfaces that give a certain amount of flexibility because they follow conventions. Dynamic APIs might be known from Resteasy Client Proxies or Spring Data’s Query Derivation . Both are interfaces that live in userland code. Resteasy/Spring Data inspect the interfaces and implement those by providing Java proxies. Method calls on these interfaces (proxies) are intercepted, inspected and translated into the according call. Let’s see how this could work for Java and Redis:

A simple command interface

public interface MyRedisCommands {
List<String> hmget(String key, String... values);
}

The interface from above declares one method: List<String > hmget(String key, String... fields) . We can derive from that declaration certain things:

It should be executed synchronously there’s no asynchronous or reactive wrapper declared in the result type The Redis command method returns a List of String s that tells us about the command result expectation, so we expect a Redis array and convert each item into a string The method is named hmget . As that’s the only detail available, we assume the command is named hmget . There are two parameters defined: String key and String... values . This tells us about the order of parameters and their types. Although Redis does not take any other parameter types than bulk strings, we still can apply a transformation to the parameters we can conclude their serialization from the declared type.

The command from above called would look like:

commands.hmget("key", "field1", "field2");

and translated to a Redis Command:

HMGET key field1 field2

The declaration on an interface comes with two interesting properties:

There’s a method signature. Although that’s an obvious fact, it is a common executable that gets called. It allows analyzing callers quickly by building searching for references to this method. There’s blank space above the method signature, ideally for documentation purposes. Multiple execution models public interface MyRedisCommands {
List<String> hmget(Timeout timeout, String key, String... values);
RedisFuture<List<String>> mget(String... keys);
Flux<String> smembers(String key);
}

A dynamic API allows variance in return types. Let’s see how this affects the things we could derive from their return types.

You already know hmget is executed in a blocking way. But wait, what’s that Timeout parameter? This is an own parameter type to declare a timeout on invocation level. The underlying execution applies timeouts from the parameter and no longer the defaults set on connection level. mget declares a RedisFuture return type wrapping a List of String . RedisFuture is a wrapper type for asynchronous execution and returns a handle to perform synchronization or method chaining in a later stage. This method could be executed asynchronously. smembers uses Flux of String . Based on the return type we can expect two properties: Flux is a reactive execution wrapper that delay execution until a subscriber subscribes to the Flux . The List type is gone because a Flux can emit 0..N items so we can decide for a streaming reactive execution. Command structure 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 requires methods to vary in name or parameter types. Variance in just the return type is supported at bytecode level but not when writing methods in your code. What if you want to declare one synchronously executed method and one that asynchronously executed taking the same parameters? You need to specify a different name. But doesn’t this clash with the previously explained name derivation? It does.

Take a closer look at mget and mgetAsync . Both methods are intended to execute the MGET command synchronously and asynchronously. mgetAsync is annotated with @Command that provides the command name to the command and overrides the assumption that the method would be named MGETASYNC otherwise. Redis is open to modules. Each module can extend Redis by providing new commands where the command pattern follows the <PREFIX>.<COMMAND> guideline. However, dots are not allowed in Java method names. Let’s apply a different naming strategy to nrRun with @CommandNaming(strategy = DOT) . Camel humps (changes in letter casing) are expressed by placing a dot between individual command segments and we’re good to run NR.RUN from Neural Redis . Some commands come with a more sophisticated syntax that does not allow just concatenation of parameters. Take a look at NR.OBSERVE . It has three static parts with parameters in between. That command structure is expressed in a command-like language. NR.OBSERVE ?0 ?1 -> ?2 TRAIN describes the command as string and puts in index references for arguments. All string parts in the command are constants and parameter references are replaces with the actual parameters. Conclusion

Applying a dynamic API to Redis shifts the view to a new perspective. It can provide a simplified custom command approach to users without sacrificing reusability. The nature of method declaration creates a place for documentation and introspection regarding its callers.

A dynamic API is beneficial also to other applications using RESP such as Disque or Tile38 .

An experimental implementation is available with lettuce from Sonatype’s OSS Snapshot repository https://oss.sonatype.org/content/repositories/snapshots/ :

<dependency>
<groupId>biz.paluch.redis</groupId>
<artifactId>lettuce</artifactId>
<version>5.0.0-dynamic-api-SNAPSHOT</version>
</dependency>

Using RedisCommandFactory

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);
} Reference @Command : Command annotation specifying a command name or the whole command structure by using a command-like language. @CommandNaming : Annotation to specify the command naming strategy. Timeout : Value object containing a timeout. RedisFuture : A Future result handle. Flux :Project Reactor publisher for reactive execution that emits 0..N items.

本文数据库(综合)相关术语:系统安全软件

主题: RedisJavaSpringReact
分页:12
转载请注明
本文标题:Command Interfaces: Approaching Redis with dynamic APIs in Java
本站链接:http://www.codesec.net/view/484569.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 数据库(综合) | 评论(0) | 阅读(40)