Back to Home

Stored Procedures in Redis

redis · lua · stored procedures

Stored Procedures in Redis

  • Tutorial
image

Many people know about the ability to store procedures in sql databases, a lot of puffy manuals and articles have been written about this. However, few people know that Redis has similar capabilities since version 2.6.0. But since Redis is not a relational database, the principles for describing stored procedures are quite different. The stored procedures in Redis are almost complete Lua scripts (at the time of writing, Lua 5.1 is used as an interpreter).

Further narrative assumes a basic introduction to the Redis API , and also that the redis-server process is running on localhost: 6379 . If you are new to Redis, then you should read a brief summary of what Redis is before reading the following material.. And also go through, at least partially, this interactive guide .

Hello world!


Using redis-cli we will return the string “Hello world!” From the database:
redis-cli EVAL 'return "Hello world!"' 0

Result:
"Hello world!"

Let's see what just happened:
  1. Calling built-in Redis commands EVAL with two arguments. First
    return "Hello world!"
    - the body of the Lua function.
    0
    - the number of Redis keys that will be passed as parameters of our function. Until we pass redis keys as parameters, i.e. specify 0.
  2. Interpreting program text on the server and returning a Lua-string value
  3. Convert Lua-string to redis bulk reply
  4. Getting the result in redis-cli
  5. redis-cli prints bulk reply to stdout


Stored procedures in Redis are ordinary Lua functions, and therefore the principle of receiving and returning arguments is similar.
Note: Lua supports mul-return (returning more than one result from a function). But in order to return multiple values ​​from redis, you need to use multi bulk reply, and from Lua tables are displayed in it, the example below will not work as you might expect:
redis-cli EVAL 'return "Hello world!", "test"' 0

"Hello world!"

The result is truncated to one return value (first).

Hello % username% !


We move on. Since functions without arguments are not of particular interest, we add the processing of arguments to our function.
According to the documentation, a function executed via EVAL can accept an arbitrary number of arguments through Lua tables KEYS and ARGV. We will use this to greet % username% if the string containing its name is passed as an argument, otherwise we will greet Habr.

We call it without arguments, the ARGV table array in Lua is empty, that is, ARGV [1] will return nil
redis-cli EVAL 'return "Hello " .. (ARGV[1] or "Habr") .. "!"' 0

Result:
"Hello Habr!"

And now we’ll pass the string “Innocent” as a parameter:
redis-cli EVAL 'return "Hello " .. (ARGV[1] or "Habr") .. "!"' 0 'Иннокентий'

Result:
"Hello \xd0\x98\xd0\xbd\xd0\xbd\xd0\xbe\xd0\xba\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb8\xd0\xb9!"

Note: Redis stores strings in utf8 and, in order to avoid any client-side problems in redis-cli, characters that are not in ascii are displayed as escape sequences. To see the readable string in bash you can do this:
echo -e $(redis-cli EVAL 'return "Hello " .. ARGV[1] .. "!"' 0 'Иннокентий')


Redis API Access from Scripts


In each Lua script, the interpreter loads these libraries:
string, math, table, debug, cjson, cmsgpack

The first 4 are standard for Lua. The last 2 are for working with json and msgpack, respectively.

In order to interact with the data in our repository, the 'redis' module was exported to Lua. Using the call function in this module, we can execute commands in the format corresponding to the commands from redis-cli .

Consider the use of redis.call with an example script that checks if a user exists in our database, and if it does, then checks for a matching login / password pair.

We will create in our database a test data set containing login-password pairs.
redis-cli HMSET 'users' 'ivan' '12345' 'maria' 'qwerty' 'oleg' '1970-01-01'

OK


Make sure that everything is really OK:
redis-cli HGETALL 'users'

1) "ivan"
2) "12345"
3) "maria"
4) "qwerty"
5) "oleg"
6) "1970-01-01"


We will give one argument to the script input, json string in the format:
{
"login":"userlogin",
"password":"userpassword"
}


The script should return 1 if the user exists and the password in json matches the password in the database, otherwise 0. If the input format is incorrect, for example, the argument was not passed to the script (ARGV [1] == nil ) or one of the required fields is missing in json , return a readable string containing error information.

For parsing and packaging, json redis exports the cjson module to Lua . In our script, we will use the decode function from this module. As a parameter, the function takes a Lua-string, which contains json, and the return value is a Lua-table, the string keys of which are json-fields.

Create a login.lua file with the following contents.
Login.lua script code
local jsonPayload = ARGV[1]
if not jsonPayload then
    return 'No such json data'
end
local user = cjson.decode(jsonPayload)
if not user.login then
    return 'User login is not set'
end
if not user.password then
    return 'User password is not set'
end
-- вызов redis API из Lua аналогичен стандартному API redis.
local expectedPassword = redis.call('HGET', 'users', user.login)
if not expectedPassword then
    return 0
end
if expectedPassword ~= user.password then
    return 0
end
return 1



Examples of using:
  1. Passwords match
    redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"qwerty"}'
    

    (integer) 1
    

  2. Passwords do not match
    redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"12345"}'
    

    (integer) 0
    

  3. There is no password field in json
    redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","pwd":"12345"}'
    

    "User password is not set"
    

  4. Argument containing json not passed
    redis-cli EVAL "$(cat login.lua)" 0
    

    "No such json data"
    



Note: All keys in Redis, as well as working with them through SET and GET, have a string representation. There is no integer type in Redis, and no float either. It is important to understand this. In the following example, we return the value of the test key as a string:
redis-cli SET test 5

OK

Find out the type of stored value:
redis-cli TYPE test

string

We will return, but already through the script:
redis-cli EVAL "return redis.call('GET', 'test')" 0

"5"


At the same time, no one forbids us to return an integer (as an integer bulk reply):
redis-cli EVAL "return tonumber(redis.call('GET', 'test'))" 0

(integer) 5


Be careful with passing Lua-number as a parameter to the redis.call function :
redis-cli EVAL "return redis.call('SET', 'test', 5.6)" 0

OK

The value is truncated to a smaller integer.
redis-cli EVAL "return tonumber(redis.call('GET', 'test'))" 0

(integer) 5

But what is really inside there:
redis-cli GET test

"5.5999999999999996"

How "right":
redis-cli EVAL "return redis.call('SET', 'test', tostring(5.6))" 0

OK

redis-cli GET test

"5.6"


Apparently, the Lua-number conversion does not go in the Lua interpreter, but in the native part of Redis written in C.

That's all for today.

see also


redis.io/commands/eval
www.redisgreen.net/blog/intro-to-lua-for-redis-programmers
redislabs.com/blog/5-methods-for-tracing-and-debugging-redis-lua-scripts

Only registered users can participate in the survey. Please come in.

Please vote on a topic worth considering in the following article:

  • 67.6% Read more about working with the Lua API. Work with multi-bulk reply 48
  • 32.3% Consider using redis in conjunction with Go using radix.v2 23

Read Next