Stored Procedures in Redis
- Tutorial

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:
- Calling built-in Redis commands EVAL with two arguments. First
- the body of the Lua function.return "Hello world!"
- 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.0 - Interpreting program text on the server and returning a Lua-string value
- Convert Lua-string to redis bulk reply
- Getting the result in redis-cli
- 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, cmsgpackThe 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.
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:
- Passwords match
redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"qwerty"}'(integer) 1 - Passwords do not match
redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"12345"}'(integer) 0 - There is no password field in json
redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","pwd":"12345"}'"User password is not set" - 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 5OKFind out the type of stored value:
redis-cli TYPE teststringWe 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) 5Be careful with passing Lua-number as a parameter to the redis.call function :
redis-cli EVAL "return redis.call('SET', 'test', 5.6)" 0OKThe value is truncated to a smaller integer.
redis-cli EVAL "return tonumber(redis.call('GET', 'test'))" 0(integer) 5But what is really inside there:
redis-cli GET test"5.5999999999999996"How "right":
redis-cli EVAL "return redis.call('SET', 'test', tostring(5.6))" 0OKredis-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