📜 ⬆️ ⬇️

“Stored Procedures” in Redis

image

Many people know about the ability to store procedures in sql databases, about it written a lot of plump manuals and articles. However, few people know that similar features exist in Redis, starting with version 2.6.0. But since Redis is not a relational database, the principles for describing stored procedures are quite different. Redis stored procedures are almost full-fledged Lua scripts (at the time of this writing, Lua 5.1 is used as an interpreter).

Further narration implies a basic familiarity with 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 the following information about 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. Call the built-in redis EVAL command with two arguments. The first
     return "Hello world!" 
    - the body of the Lua function.
     0 
    - the number of keys Redis, which will be transferred as parameters of our function. As long as we do not pass redis keys as parameters, i.e. we specify 0.
  2. Interpreting the program text on the server and returning the Lua-string values
  3. Converting Lua-string to redis bulk
  4. Getting results in redis-cli
  5. redis-cli prints bulk reply to stdout

')
Redis stored procedures are normal Lua functions, and therefore the principle of getting and returning arguments is similar.
Note: Lua supports mul-return (returning more than one result from a function). But in order to return several 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 (the first).

Hello % username% !


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

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

Result:
 "Hello Habr!" 

And now, as a parameter, pass the string "Innocent":
 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 problems on the client side, redis-cli characters that are not in ascii are output as escape sequences. To see the readable line in bash, you can do this:
 echo -e $(redis-cli EVAL 'return "Hello " .. ARGV[1] .. "!"' 0 '') 


Accessing the Redis API from scripts


The interpreter loads these libraries into each Lua script:
 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 module 'redis' is 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 on the example of a script that checks whether a user exists in our database, and if it exists, it checks the matching of the login-password pair.

Let's create in our database a test dataset containing pairs of login - password.
 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" 


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


The script must 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 json is missing one of the required fields , return a readable string containing the error information.

Json redis exports the cjson module to Lua for parsing and packaging. 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. Json is missing a password field
     redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","pwd":"12345"}' 

     "User password is not set" 

  4. Json argument not passed
     redis-cli EVAL "$(cat login.lua)" 0 

     "No such json data" 



Note: All keys in Redis, as well as working with them via SET and GET, have a string representation. Redis does not have an integer type, and there is no float either. It is important to understand. 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 through the script:
 redis-cli EVAL "return redis.call('GET', 'test')" 0 

 "5" 


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

 (integer) 5 


Be careful with passing a 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:
 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 is not 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

Source: https://habr.com/ru/post/270251/


All Articles