Redis Lua Scripting: How To Get Started With?

Posted October 25, 2017 - 4 min read
Topics:  

Previously we have discussed Redis transactions and have learned that Redis provides two approaches for using transactions: MULTI/EXEC and Lua scripts.

Lua scripts are more flexible as they allow to use the full potential of the Lua programing language and overcome the limitation of MULTI/EXEC which does not give us the ability to use the result of a given command in the next command in the context of the same transaction.

Within this post we are going to learn how to use Lua scripts.

Redis Lua Scripting Overview

Lua scripts are executed in the Redis server and allow to efficiently access and manipulate stored data by reducing network latency and resources.

A Lua script is executed as a single block operation and while being executed it blocks the server.

To work with Lua scripts, Redis provides SCRIPT LOAD and EVALSHA commands.

Loading Scripts

SCRIPT LOAD script_content

SCRIPT LOAD command allows to upload a Lua script to Redis server in order to be parsed and stored into the scripts cache.

SCRIPT LOAD returns the SHA1 digest of the script and may be called multiple times with the same script without warring about any side effects. It will always return the same hash value.

This may be useful for your application as it does not have to hash and track the scripts itself.

redis> SCRIPT LOAD "return 'Hello world!'"
"47a013e660d408619d894b20806b1d5086aab03b"

Executing Scripts

EVALSHA script_digest keys_count [key [key ...]] [arg [arg ...]]

The EVALSHA command allows to execute a script and accepts the following arguments:

  • script_digest - The script digest which has been returned after running SCRIPT LOAD command.
  • keys_count - The number of Redis keys that are being passed in within the arguments.
  • [key [key ...]] - Any number of Redis keys. Redis keys are passed in after keys_count and before any the other argument.
  • [arg [arg ...]] - Any other arguments (values) that are used in your script.

A Lua script may use one or more Redis keys to access stored data.

It is recommended to always pass in Redis keys as arguments and not to hardcode them in the script itself.

Otherwise, in case of a cluster configuration for example, when hardcoded, Redis would not be able to parse the keys and figure out in which node the data is stored.

In addition to key names, the script arguments also include any other parameter that may be used during the script execution.

Redis keys and other arguments that has been passed in to a given script are stored respectively under KEYS and ARGV global variables that are accessible to the script.

redis> EVALSHA 47a013e660d408619d894b20806b1d5086aab03b 0
"Hello world!"

Real World Usage Example

Lua is very intuitive and simple to learn, and you should not be scarred of it.

We will use Node.js, TypeScript, and node-redis to demonstrate how to work with Redis scripts from our application.

Let’s create a simple Lua script and save it, for example, under /redis-scripting/extend.lua.

The content of this file is:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("PEXPIRE", KEYS[1], ARGV[2])
end
return 0

In the script above, before setting a timeout on KEYS[1] we always check that its value is equal to ARGV[1].

Each time we start our application, we need to make sure that such script is uploaded to Redis server.

import { promises } from 'fs';
import { createClient } from 'redis';

const client = createClient();

const read = async (path: string): Promise<string> => {
  const content = await promises.readFile(path);
  return content.toString();
};

const load = async (content: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    client.script('load', content, (err, digest) => {
      if (err) reject(err);
      else resolve(digest);
    });
  });
};

const exec = async (
  digest: string,
  key: string,
  value: string,
  timeout: number,
): Promise<number> => {
  return new Promise((resolve, reject) => {
    client.evalsha([digest, 1, key, value, timeout], (err, res) => {
      if (err) reject(err);
      else resolve(res);
    });
  });
};

read('/redis-scripting/extend.lua')
  .then((content) => load(content))
  .then((digest) => exec(digest, 'some_key', 'some_key_value', 3000))
  .then((result) => {
    if (result) console.log('Timeout has been set');
    else console.log('Timeout has not been set');
  });

Within this example, we have read the content of our Lua script, uploaded it to Redis server, and after that we have executed it.

You should pay attention to how the client.script and client.evalsha methods are used.

For more details about these methods you should refer to node-redis documentation.

Conclusion

As we have seen, Redis scripting is not a hard thing to learn and allows you to add new features to Redis.

Nevertheless, one downside of Redis scripting is that it monopolizes the server.

You should always remember that while your script is running everything else on the server is put on hold.