Using Lua Scripts for Redis Transactions

Summary
Redis transactions can be implemented with `MULTI` or with Lua scripts. Lua scripts are often simpler and more efficient because they reduce round trips and can be cached.

Redis transactions can be implemented in two ways: with MULTI, or with Lua scripts. Lua scripts are simpler, require fewer client-server interactions, and can be cached by Redis, which makes them especially useful in performance-sensitive scenarios.

Lua Scripts

Official documentation: https://redis.io/commands/eval

Redis provides two commands for executing Lua scripts: eval and evalsha.

The syntax of eval is:

1
2
3
4
5
6
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2], ARGV[3]}" 2 key1 key2 first second third
1) "key1"
2) "key2"
3) "first"
4) "second"
5) "third"

Explanation:

  • The second string argument is the Lua script itself. KEYS and ARGV are reserved keywords.
  • 2 means the script references two keys.
  • KEYS is used to pass Redis keys into the script.
  • ARGV contains the remaining arguments after the keys, indexed from 1.
  • return specifies the returned value.

If the server has already cached the Lua script, you can use evalsha instead. This reduces payload size when the script body is large.

Starting from Redis 3.2.0, Redis also ships with a Lua debugger: https://redis.io/topics/ldb

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "github.com/gomodule/redigo/redis"

// Redis Lua script that guarantees atomic reservation when allocating multiple IDs.
func ReserveUserToken(conn redis.Conn, accountID uint64, howmany uint32) (latest uint64, err error) {
    ss := `local sid=redis.call('HGET',KEYS[1],'latest') or 0; `
    for i := 0; i < int(howmany); i++ {
        ss += `redis.call('HSET', KEYS[1], sid, ARGV[1]); sid=sid+1; `
    }
    // Update latest
    ss += `redis.call('HSET', KEYS[1], 'latest', sid); return sid`
    script := redis.NewScript(1, ss)

    key := fmt.Sprintf("userTokens:%d", accountID)
    ts := time.Now().Unix()

    resp, err := script.Do(conn, key, ts)
    if err != nil {
        return
    }
    latest = uint64(resp.(int64))
    return
}

This code works as follows:

  1. It creates a Redis hash named userToken:{accountId} for each user if it does not already exist, and initializes the latest field to 0.
  2. If howmany is not zero, it reserves that many tokens for the user by:
    • incrementing latest and using the new value as the hash field
    • writing the reserved timestamp into the hash
    • repeating this in a loop
  3. It writes the final latest value back to the hash and returns it.

One important detail is the trailing or 0 in local sid=redis.call('HGET',KEYS[1],'latest') or 0;. Without it, sid may become a boolean value when the field does not exist, and arithmetic on it will fail with an error like this:

1
(error) ERR Error running script (call to f_2bd1a2e80fc04313100937b7b533da6bea026773): @user_script:1: user_script:1: attempt to perform arithmetic on local 'sid' (a boolean value)

Additional Notes

Lua conditionals

The syntax for conditionals in Lua is:

1
2
3
4
5
if cond then
    clause
else
    clause
end

Without an else, it can be shortened to:

1
2
3
if cond then
    clause
end

Lua also supports elseif.

Integers and strings

Redis keys are strings, while values can be integers. If your key itself looks numeric and you compare it directly with an integer value, you may get unexpected results because the key is still a string. If you need numeric comparison, use one of these approaches:

  1. Convert with tonumber
  2. Force conversion with +0

JSON handling

Lua scripts inside Redis have the cjson library built in, so you can use cjson.encode and cjson.decode directly.

An encode example:

1
2
3
4
5
6
7
local user={name=ARGV[1],year=ARGV[2],address=ARGV[3]};
local userJson=cjson.encode(user);
if redis.call('HSET', 'users', ARGV[1], userJson) == 0 then
    return -1
else
    return userJson
end

decode is similar:

1
2
local userJson = redis.call('HSET', 'users', ARGV[1]);
local user = cjson.decode(userJson)