Redis provides a very rich instruction set, but users are still not satisfied with it. They hope to customize and expand several instructions to complete some problems in specific fields. Redis provides lua scripting support for such user scenarios, where users can send Lua scripts to the server to perform custom actions and retrieve the script response data. The Redis server executes the Lua script atomically, single-threaded, ensuring that the lua script is not interrupted by any other request during processing.
For example, in the distributed locking section, we mentioned the del_if_EQUALS directive, which combines matching keys and deleting keys together to execute atomically. Redis does not provide this function natively, but it can be done using Lua scripts.
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Copy the code
How does this script work? Use the EVAL instruction
127.0.0.1:6379 >setFoo bar OK 127.0.0.1:6379 >eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
(integer6379 > 1 127.0.0.1) :eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
(integer) 0
Copy the code
The first argument to the EVAL instruction is the script content string. In the example above, we compressed the Lua script into a single line surrounded by single quotes for easy command line execution. Then the number of keys and each key string, and finally a series of additional parameter strings. The number of additional parameters does not need to be the same as that of the key.
EVAL SCRIPT KEY_NUM KEY1 KEY2 ... KEYN ARG1 ARG2 ....
Copy the code
The above example has only one key, which is foo, followed by bar, which is the only additional argument. In Lua scripts, array subscripts start at 1, so KEYS[1] gets the first key, and ARGV[1] gets the first additional argument. The redis. Call function allows us to call the native redis directive, which calls the GET and del directives, respectively. The result returned by return is returned to the client.
SCRIPT LOAD and EVALSHA directives
In the example above, the content of the script is short. If the script content is long and the client needs to execute it frequently, passing the lengthy script content each time can be a waste of network traffic. So Redis also provides SCRIPT LOAD and EVALSHA directives to solve this problem.
incrby key value ==> $key = $key + value
mulby key value ==> $key = $key * value
Copy the code
We now use SCRIPT LOAD and the EVALSHA directive to perform the self-multiplication operation.
local curVal = redis.call("get", KEYS[1])
if curVal == false then
curVal = 0
else
curVal = tonumber(curVal)
end
curVal = curVal * tonumber(ARGV[1])
redis.call("set", KEYS[1], curVal)
return curVal
Copy the code
Start with the above script as a single line, separated by a semicolon
local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal
Copy the code
Load scripts
127.0.0.1:6379 > script load'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal'
"be4f93d8a5379e5e5b768a74e77c8a4eb0434441"
Copy the code
The command line outputs a long string that is the unique identifier of the script, which we will use to execute the instruction
1 notexistskey 5 (127.0.0.1:6379 > evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441integer) 0 127.0.0.1:6379 > evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 notexistskey 5 (1integer) 0 127.0.0.1:6379 >setFoo 1 OK 127.0.0.1:6379 > evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 5 (foointeger1) 127.0.0.1:6379 > evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 foo (5integer25)Copy the code
Error handling
The script argument above requires that the additional arguments passed in must be integers. What if integers are not passed?
127.0.0.1:6379 > evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo bar (error) ERR error running script (call to f_be4f93d8a5379e5e5b768a74e77c8a4eb0434441): @user_script:1: user_script:1: attempt to perform arithmetic on a nil valueCopy the code
Note that this is a dynamically thrown exception. Redis protects the main thread from crashing the server due to errors in the script, similar to having a large try catch statement wrapped around the script. An error was encountered during the execution of a Lua script. As with redis transactions, instructions that have been executed through the redis. Call function cannot be undone.
Int luaCreateFunction(client *c, lua_State *lua, char * funcName, robj *body) {...if(lua_pcall (lua, 0, 0)) {addReplyErrorFormat (c,"Error running script (new function): %s\n",
lua_tostring(lua,-1));
lua_pop(lua,1);
returnC_ERR; }... } // run voidevalGenericCommand(client *c, int evalsha) { ... Err = lua_pcall (lua, 0, 1, 2); . }Copy the code
In addition to the redis.call function, Redis also provides the redis.pcall function in lua scripts. The former throws an exception on an error, while the latter returns an error message. Caution When using the call function, script execution may be interrupted if an error occurs. Use the call function with caution to ensure atomicity of the script.
Error transfer
The redis. Call function calls generate errors. What information does the script return when it encounters such errors? Let’s do another example
127.0.0.1:6379> hset foo x 1 y 2
(integer6379 > 2 127.0.0.1) :eval 'return redis.call("incr", "foo")' 0
(error) ERR Error running script (call to f_8727c9c34a61783916ca488b366c475cb3a446cc): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value
Copy the code
The client still outputs a generic error message, rather than the WRONGTYPE error message returned by the INCR call. Inside Redis, an exception is thrown up when Redis. Call encounters an error, and the peripheral pCall, invisible to the user, returns a generic error message to the client when it catches a script exception. If we had changed the call above to pCall, the result would have been different, and it could have passed up specific errors returned by internal instructions.
127.0.0.1:6379 >eval 'return redis.pcall("incr", "foo")' 0
(error) WRONGTYPE Operation against a key holding the wrong kind of value
Copy the code
What about script deadloops?
Redis commands are executed by a single thread, which also executes lua scripts from the client. If lua scripts have an infinite loop, is Redis doomed? Redis solves this problem by providing the Script kill directive to dynamically kill a Lua script that has run out of time. Script kill does not change the internal data state of Redis, because Redis does not allow script kill to break the atomicity of script execution. Call (“set”, key, value) is used to modify the internal data of the script. The server will return an error when script kill is executed. Let’s try the following script kill instruction.
127.0.0.1:6379 >eval 'while(true) do print("hello") end' 0
Copy the code
After the eval command is executed, it is obvious that Redis is dead and there is no response. If you look at the Redis server log, you can see that the log frantically outputs hello strings. At this point, a redis-CLI must be restarted to execute script kill.
127.0.0.1:6379 > scriptkillOK (2.58 s)Copy the code
Back to the output of the eval instruction
127.0.0.1:6379 >eval 'while(true) do print("hello") end'0 (error) ERR Error running script (call to f_d395649372f578b1a0d3a1dc1b2389717cadf403): @user_script:1: Script killed by user with SCRIPT KILL... (6.99 s)Copy the code
See here careful students will notice two doubts, the first is why script kill command execution 2.58 seconds, the second is the script is dead, where Redis free time to accept script kill command. If you try to connect to the server in the second window by executing redis-cli, you will also find a third doubt, the redis-CLI connection is a bit slow, about a second or so.
The principle of Script Kill
The Lua scripting engine is so powerful that it provides a variety of hook functions that allow you to run hook code while executing instructions from the internal virtual machine. For example, Redis uses a hook function that executes every N instructions.
void evalGenericCommand(client *c, int evalsha) { ... Lua executes the hook function luaMaskCountHook lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000) once every 10W instructions. . }Copy the code
Redis takes a few breaks in the hook function to process client requests, and only does so if the Lua script has timed out, which is 5 seconds by default. So the three questions raised above disappear.
To consider
In the section of delay queue, we use zrangebyScore and Zdel to compete for the task in the delay queue, and determine which client grabs the task by the return value of Zdel. This means that clients who don’t get the task will feel bad about having the meat on their lips taken from them by someone else. The zrangebyScore and Zdel instructions are atomically executed without this problem if you can use Lua scripts to implement the scramble logic, so give it a try.
Note: If you are not familiar with Lua, it is recommended to learn Lua first. Lua is easy to learn, but it can not be learned in a few minutes. You need to come to a small volume of content. This small volume focuses on Redis, so I will not open a large part of the content to detail lua, friends who need to search for relevant online tutorials.
To read more in-depth technical articles, scan the QR code above to follow the wechat public account “Code Hole”.