Skip to content

Scripting and Functions

Mark Paluch edited this page Oct 18, 2023 · 2 revisions

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.

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.

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:

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");

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.

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-link} 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:

local function my_hlastmodified(keys, args)
  local hash = keys[1]
  return redis.call('HGET', hash, '_last_modified_')
end
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:

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.

Clone this wiki locally