Introduction to the combination of Lua script and Redis database

  • 2020-05-13 03:48:47
  • OfStack

You've probably heard of the scripting language embedded in Redis, but you haven't tried it yet, have you? This introductory tutorial will help you learn to use the powerful lua language on your Redis server.
Hello, Lua!

Our first Redis Lua script simply returns a string and does not interact with redis in any meaningful way.

local msg = "Hello, world!"
return msg

This is very simple, line 1 defines a local variable msg to store our information, and line 2 indicates that the msg value is returned from the redis server to the client. Save the file to hello.lua and run it like this:

redis-cli EVAL "$(cat hello.lua)" 0

Running this code will print "Hello,world!" , EVAL in the first parameter is our lua script, which we use the cat command to read the content of our script from the file. The second parameter is the number of the Redis key that the script needs to access. Our simple "Hello Script" does not access any keys, so we use 0

Access keys and parameters

Let's say we want to set up an URL shorthand server. We are going to store each incoming URL and return a unique 1 value so that we can access the URL later.

We will use the Lua script to immediately retrieve a unique 1 identifier ID from Redis using INCRand, and use this identifier ID as the key value of URL stored in a hash:

local link_id = redis.call("INCR", KEY[1])
redis.call("HSET", KEYS[2], link_id, ARGV[1])
return link_id

We will access Redis for the first time using the call() function. The argument to call() is the command to Redis: INCR first < key > And then HSET < key > < field > < value > . The two commands will be executed in turn -- Redis will do nothing when this script is executed, and it will run very quickly.

We will access two Lua tables: KEYS and ARGV. Forms are the Lua one-only mechanism for associative arrays and structured data. For our purposes, you can think of them as an array of peers in any language you're familiar with, but note the two Lua rules that can easily confuse beginners:

The table is based on 1, which means the index starts with a value of 1. So the first element in the table is mytable[1], the second is mytable[2], and so on. You cannot have an nil value in the table. If an operation table has [1, nil, 3, 4], the result will be [1] -- the table will be truncated at the first nil.

When calling this script, we also need to pass the KEYS and ARGV table values:

redis-cli EVAL "$(cat incr-and-stor.lua)" 2 links:counter links:urls http://malcolmgladwellbookgenerator.com/


In the EVAL statement, 2 indicates the number of KEY to be passed in, followed by the two KEY to be passed in, and finally the value of ARGV to be passed in. When the Lua script is executed in Redis, Redis-cli checks the number of KEY passed in, unless it is a command only.

For a clearer explanation, the following scripts replace KEY and ARGV:

local link_id = redis.call("INCR", "links:counter")
redis.call("HSET", "links:urls", link_id, "http://malcolmgladwellbookgenerator.com")
return link_id

When writing Lua scripts for Redis, each KEY is specified through the KEYS table. The ARGV table is used to pass the parameters, and in this case ARGV is used to pass URL.

Logical conditions: increx and hincrex

The last example saved the link as a short url. To find out how many times the link has been clicked, add an hash counter to Redis. When a user with a link tag accesses it, we check whether it exists. If it does, we need to add 1 to the counter:

if redis.call("HEXISTS", KEYS[1], ARGV[1]) == 1 then
return redis.call("HINCR", KEYS[1], ARGV[1])
else
return nil
end

Every time someone clicks on the short url, we run this script to keep track of the link being Shared again. We invoke the script with EVAL, passing in inlinks:visits (keys[1]) and the link id returned by the previous script (ARGV[1]).

This script checks if the same hash exists, and if so, adds 1 to the standard Redis KEY.

if redis.call("EXISTS",KEYS[1]) == 1 then
return redis.call("INCR",KEYS[1])
else
return nil
end

Script loading and registration execution

Note that when Redis is running the Lua script, nothing else gets done! Scripts are best left to simply extend Redis for smaller atomic operations and simple logical control needs, and bug in Lua scripts may trigger the entire Redis server lock -- it's best to keep the scripts short and easy to debug.

While these scripts are generally short, we'd prefer not to use the full Lua script every time we execute it. Instead, we can register the Lua script (or at your deployment time) in step by step development of the program, and then call it with the SHA-1 id generated after registration.

redis-cli SCRIPT LOAD "return 'hello world'"
=> "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
 
redis-cli EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
=> "hello world"


It is usually not necessary to show that a call to SCRIPT LOAD is implicitly loaded when a program executes EVAL. The program will try EAVALSHA first, and EVAL will be called when the script is not found.

For Ruby developers, see 1 for Shopify's Wolverine, which simply loads and stores Lua scripts for Ruby. For PHP developers, Predis supports loading Lua scripts and calling them as normal Redis commands. If you use these or other tools to standardize your interaction with Lua, let me know that I'm interested in learning more outside of this article.

When to use Lua

Redis supports blocks such as WATCH/MULTI/EXEC, which can perform 1 set of operations and 1 commit execution, seemingly overlapping with Lua. How do you choose? All operations in the MULT block are independent, but in Lua, subsequent operations can depend on the results of previous operations. Using the Lua script at the same time also avoids the slow client response caused by WATCH's use of race conditions.

In RedisGreen, we see many applications using Lua as well as MULTI/EXEC, but the two are not substitutes. Many successful Lua scripts are small, implementing only one of your application's needs while the Redis command does not have single 1 functionality.

Access to library

The Lua interpreter for Redis loads seven libraries :base, table, string, math, debug, cjson and cmsgpack. The first few are standard libraries that allow you to perform basic operations in any language. The latter two allow Redis to support JSON and MessagePack -- very useful features, and I wonder why you often don't see this use.

Web applications often use JSON as api to return data. You might also be able to save 1 heap of JSON data into key of Redis. When you want to access some JSON data, you first need to save it to an hash. JSON support for Redis is very convenient:

if redis.call("EXISTS", KEYS[1]) == 1 then
local payload = redis.call("GET", KEYS[1])
return cjson.decode(payload)[ARGV[1]]
else
return nil
end


Here we check to see if key exists, or if it does not, we quickly return nil. If it exists, get the JSON value from Redis, parse it with cjson.decode (), and then return the requested content.

redis-cli set apple '{ "color": "red", "type": "fruit" }'
=> OK
 
redis-cli eval "$(cat json-get.lua)" 1 apple type
=> "fruit"

Load this script into your Redis server and save JSON data to Redis, usually hash. Although we have to parse it every time we access it, as long as your object is small, this is actually very fast.

If your API is only available internally, it's usually a matter of efficiency. MessagePack is a better choice than JSON, which is smaller and faster. In Redis (and more often), MessagePack is a better alternative to JSON.

if redis.call("EXISTS", KEYS[1]) == 1 then
  local payload = redis.call("GET", KEYS[1])
  return cmsgpack.unpack(payload)[ARGV[1]]
else
  return nil
end


Numerical transformation

Lua and Redis each have their own set of types, so it is important to understand that the values of Redis and Lua are changed by the conversion between the boundary calls. 1 from Lua number returns to Redis client as integer - any decimal point after the number is cleared:

local indiana_pi = 3.2
return indiana_pi

When you run this script, Redis will return an integer of 3, missing the useful fragment in pi. It seems simple enough, but once you start doing Redis interacting with intermediate scripts, you need to be more careful. Such as:

local indiana_pi = 3.2
redis.call("SET", "pi", indiana_pi)
return redis.call("GET", "pi")

The result of the execution is 1 string: "3.2". Why is that? There is no proprietary numeric type in Redis. When we call SET the first time, Redis has already saved it as a string, and when we initialize Lua it is lost as the type information of a floating point number. So when we pull this value out later, it becomes a string.

In Redis, except INCR and DECR, the data accessed by other GET and SET operations are treated as strings. INCR and DECR are specialized numeric operations that actually return integer (integer) replies (maintain and store according to numeric rules), but Redis internal save types are actually string values.

Conclusion:

The following are some common mistakes when using Lua in Redis:

The table is an expression in Lua, which is different from many popular languages. The first element in KEYS is KEYS[1], and the second element is KEYS[2]. nil is the end of the table, [1,2,nil,3] will automatically become [1,2], so do not use nil in the table. redis.call will trigger an exception in Lua, and redis.pcall will automatically catch all errors that can be detected and return them as a table. All Lua Numbers will be converted to integers, the decimal points sent to Redis will be lost, and they will be converted to a string type before being returned. Make sure that all KEY used in Lua are in the KEY table, otherwise there is a risk that your script will not be well supported in future Redis releases. The Lua script, like other Redis operations 1, will not run while the script is executing. Consider scripting to support the Redis server, but keep it short and useful.


Related articles: