If you are not familiar with Redis, then you should go check it out. It’s a persistent cache that has many great features. Of course it’s not really a database, so preserving atomicity during transactions can be tricky, and there are no rollbacks. Redis operates in a single thread – so if it can be persuaded to execute all your commands as a single activity then it will do it without allowing itself to be interrupted by anything else, and it provides a set of commands such as MULTI, EXEC, WATCH, DISCARD to help you make that happen.
However, if your transactions contains commands that are dependent on the values returned by earlier commands then this rudimentary transaction support doesn’t really work, so for my Ephemeral Exchange cross platform cache manager I needed to figure out a better approach.

Lua

That’s where Lua comes in.  Lua is a scripting language used in a wide variety of situations, and yes, Redis supports Lua as its scripting platform. These are scripts that execute on the Redis server. The native support for scripting is through the EVAL and EVALSHA Redis commands and is a little convoluted, but Redis clients usually provide a simplified way to define and execute Lua scripts.

ioredis

The Redis client I use is ioredis, and it provides a defineCommand method for creating and registering scripts.
In this very simple example I’m defining a Redis command called ‘insert_if_missing’, using a redis handle called redisIntent_ and its purpose is to create an entry, but only if it doesn’t already exist.
  redisIntent_.defineCommand('insert_if_missing', {
      numberOfKeys: 1,
      lua: `if redis.call("exists", KEYS[1]) == 1 then
              return tonumber(redis.call("ttl" , KEYS[1]))
            else
              redis.call ("set",KEYS[1],ARGV[1])
              redis.call ("expire",KEYS[1],tonumber(ARGV[2]))
              return nil
            end`
    });
Later on, I can use it like this in my client App.
return redisIntent_.insert_if_missing(key,data,lifetime)
        .then (function (e) {
          if(e === null) {
            // item  was created
          } 
          else {
           // item already existed, and it has e seconds before expiring
          }

Lua syntax

It’s a fairly comprehensive, compact language, but I’ll walk through this small Redis implementation, but first a word about KEYS and ARGV which is the method for passing arguments to your Lua script. In Lua terms, these are known as tables, but you can think of them as Arrays.
  • KEYS is an array (first element is index 1) of keys that are required to be passed to the script.
  • ARGV is an array (first element is index 1) of additional values that can be passed to the script.
The ioedis defineCommand allows you to refer to a named script as a custom method of the Redis handle, and for you to pass KEYS and ARGV elements to it when you call that method. Native Redis command can be accessed server-side, from with a Lua script, with redis.call(redicommand, args…).
I’m calling my script like this from the client-side, so as far as Lua is concerned, key = KEYS[1], data = ARGV[1] and lft = ARGV[2]
redisIntent_.insert_if_missing(key,data,lifetime).then (.... do something );
And on the server
'if redis.call("exists", KEYS[1]) == 1 then
              return tonumber(redis.call("ttl" , KEYS[1]))
            else
              redis.call ("set",KEYS[1],ARGV[1])
              redis.call ("expire",KEYS[1],tonumber(ARGV[2]))
              return nil
end if'
which does this
  • calls the redis command EXISTS with the argument of key
  • if it exists (response is 1), call the redis command TTL to get the time to live, convert it to a number and return that to the server
  • if it doesn’t exist, call the redis command SET key data, then set it to expire using EXPIRE key lifetime, then return nil (which converts to null)
And that’s how you can use Lua scripting to preserve atomicity in Redis.
Why not join our forum, follow the blog or follow me on Twitter to ensure you get updates when they are available.