"After all, the engineers only needed to refuse to fix anything, and modern industry would grind to a halt." -Michael Lewis

Enable Massive Growth

Pre Loading a Lua Script into Redis With Lettuce

Apr 2021

The source code for what follows can be found on Github.

In my last article on running a lua script against redis with lettuce, we just sent the entire script [that redis will execute atomically] along with the arguments every time. For very small scripts this is unlikely to be a problem, but there is definitely a more efficient way to do this, using EVALSHA.

How EVALSHA Works

Running a lua script without evalsha means that we send the script and the arguments every time, like we have covered already:

redis-cli eval "return redis.call('set',KEYS[1],ARGV[1],'ex',ARGV[2])" 1 foo1 bar1 10
OK

SCRIPT LOAD allows us to tell redis "this is my script, please remember it", then we can use EVALSHA to run the script that redis now remembers. For example, using the CLI:

redis-cli
> SCRIPT LOAD "return redis.call('set',KEYS[1],ARGV[1],'ex',ARGV[2])"
"cf4df3d8eb7f521ceb285c6870e5713d79e2bb0b"
> evalsha cf4df3d8eb7f521ceb285c6870e5713d79e2bb0b 1 foo1 bar1 10
OK

We can verify that works with a shell script like:

$ SHA=$(redis-cli script load "return redis.call('set',KEYS[1],ARGV[1],'ex',ARGV[2])")
$ redis-cli evalsha "$SHA" 1 foo1 bar1 10; redis-cli ttl foo1; redis-cli get foo1      
OK
(integer) 10
"bar1"

By referencing the hash of the script [sha1 hash, to be more specific], we don't have to send the entire script. Indeed, regardless of how big a script we load, the size of the hash that represents the script will stay compact.

EVALSHA with Lettuce

EVALSHA with lettuce can work much the same way, if we want it to. Just load the script and used the returned hash [note that the SHA1 hash is represented as a hexadecimal string]:

    @Test
    public void scriptLoadFromResponse() {
        String shaOfScript = redisReactiveCommands.scriptLoad(SAMPLE_LUA_SCRIPT).block();

        StepVerifier.create(redisReactiveCommands.evalsha(
                shaOfScript,
                ScriptOutputType.BOOLEAN,
                // keys as an array
                Arrays.asList("foo1").toArray(new String[0]),
                // other arguments
                "bar1", "10")
        )
                .expectNext(true)
                .verifyComplete();
    }

If you want to generate the hash of the script yourself, there are several libraries available to you. Just get the SHA1 hash of the script [assuming UTF-8 encoding, which java strings are], then encode the output into a hex string. We can see that the response from redis and our code generate the same sha:

    @Test
    public void scriptLoadFromDigest() throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        byte[] digestAsBytes = md.digest(SAMPLE_LUA_SCRIPT.getBytes(StandardCharsets.UTF_8));
        String hexadecimalStringOfScriptSha1 = Hex.encodeHexString(digestAsBytes);
        String hexStringFromRedis = redisReactiveCommands.scriptLoad(SAMPLE_LUA_SCRIPT).block();

        // they're the same
        assertEquals(hexadecimalStringOfScriptSha1, hexStringFromRedis);

        StepVerifier.create(redisReactiveCommands.evalsha(
                hexadecimalStringOfScriptSha1,
                ScriptOutputType.BOOLEAN,
                // keys as an array
                Arrays.asList("foo1").toArray(new String[0]),
                // other arguments
                "bar1", "10")
        )
                .expectNext(true)
                .verifyComplete();
    }

Note that I'm using an apache commons library to take the byte array and encode it to hex in this case. The internet has several suggestions for how you can encode a byte array to a hex string yourself if you don't like the way I've done it here.

Remember that the primary benefit here is that you don't have to send the script over the wire and have redis decode it every single time, which can be significant if used frequently.

Nick Fisher is a software engineer in the Pacific Northwest. He focuses on building highly scalable and maintainable backend systems.