Redis 如何应对并发访问

上个文章中,我们分析了Redis 中命令的执行是单线程的,虽然 Redis6.0 版本之后,引入了 I/O 多线程,但是对于 Redis 命令的还是单线程去执行的。所以如果业务中,我们只用 Redis 中的单命令去处理业务的话,命令的原子性是可以得到保障的。

但是很多业务场景中,需要多个命令组合的使用,例如前面介绍的 读取-修改-写回 场景,这时候就不能保证组合命令的原子性了。所以这时候 Lua 就登场了。

使用 Lua 脚本

Redis 在 2.6 版本推出了 Lua 脚本功能。

引入 Lua 脚本的优点:

1、减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。

2、原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。

3、复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

关于 Lua 的语法和 Lua 是一门什么样的语言,可以自行 google。

Redis 中如何使用 Lua 脚本

redis 中支持 Lua 脚本的几个命令

redis 自 2.6.0 加入了 Lua 脚本相关的命令,在 3.2.0 加入了 Lua 脚本的调试功能和命令 SCRIPT DEBUG。这里对命令做下简单的介绍。

EVAL:使用改命令来直接执行指定的Lua脚本;

SCRIPT LOAD:将脚本 script 添加到脚本缓存中,以达到重复使用,避免多次加载浪费带宽,该命令不会执行脚本。仅加载脚本缓存中;

EVALSHA:执行由 SCRIPT LOAD 加载到缓存的命令;

SCRIPT EXISTS:以 SHA1 标识为参数,检查脚本是否存在脚本缓存里面

SCRIPT FLUSH:清空 Lua 脚本缓存,这里是清理掉所有的脚本缓存;

SCRIPT KILL:杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效;

SCRIPT DEBUG:设置调试模式,可设置同步、异步、关闭,同步会阻塞所有请求。

EVAL

通过这个命令来直接执行执行的 Lua 脚本,也是 Redis 中执行 Lua 脚本最常用的命令。

  1. EVAL script numkeys key [key ...] arg [arg ...]

来看下具体的参数

  • script: 需要执行的 Lua 脚本;

  • numkeys: 指定的 Lua 脚本需要处理键的数量,其实就是 key 数组的长度;

  • key: 传递给 Lua 脚本零到多个键,空格隔开,在 Lua 脚本中通过 KEYS[INDEX] 来获取对应的值,其中1 <= INDEX <= numkeys

  • arg: 自定义的参数,在 Lua 脚本中通过 ARGV[INDEX] 来获取对应的值,其中 INDEX 的值从1开始。

看了这些还是好迷糊,举个栗子

  1. 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2],ARGV[3]}" 2 key1 key2 arg1 arg2 arg3
  2. 1) "key1"
  3. 2) "key2"
  4. 3) "arg1"
  5. 4) "arg2"
  6. 5) "arg3"

可以看到上面指定了 numkeys 的长度是2,然后后面 key 中放了两个键值 key1 和 key2,通过 KEYS[1],KEYS[2] 就能获取到传入的两个键值对。arg1 arg2 arg3 即为传入的自定义参数,通过 ARGV[index] 就能获取到对应的参数。

一般情况下,会将 Lua 放在一个单独的 Lua 文件中,然后去执行这个 Lua 脚本。

执行语法 --eval script key1 key2 , arg1 age2

举个栗子

  1. # cat test.lua
  2. return {KEYS[1],KEYS[2],ARGV[1],ARGV[2],ARGV[3]}
  3. # redis-cli --eval ./test.lua key1 key2 , arg1 arg2 arg3
  4. 1) "key1"
  5. 2) "key2"
  6. 3) "arg1"
  7. 4) "arg2"
  8. 5) "arg3"

需要注意的是,使用文件去执行,key 和 value 用一个逗号隔开,并且也不需要指定 numkeys。

Lua 脚本中一般会使用下面两个函数来调用 Redis 命令

  1. redis.call()
  2. redis.pcall()

redis.call() 与 redis.pcall() 很类似, 他们唯一的区别是当redis命令执行结果返回错误时, redis.call() 将返回给调用者一个错误,而 redis.pcall() 会将捕获的错误以 Lua 表的形式返回。

  1. 127.0.0.1:6379> EVAL "return redis.call('SET','test')" 0
  2. (error) ERR Error running script (call to f_77810fca9b2b8e2d8a68f8a90cf8fbf14592cf54): @user_script:1: @user_script: 1: Wrong number of args calling Redis command From Lua script
  3. 127.0.0.1:6379> EVAL "return redis.pcall('SET','test')" 0
  4. (error) @user_script: 1: Wrong number of args calling Redis command From Lua script

同样需要注意的是,脚本里使用的所有键都应该由 KEYS 数组来传递,就像这样:

  1. 127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
  2. OK

下面这种就是不推荐的

  1. 127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
  2. OK

原因有下面两个

1、Redis 中所有的命令,在执行之前都会被分析,来确定会对那些键值对进行操作,对于 EVAL 命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行;

2、使用正确的形式来传递键还有很多其他好处,它的一个特别重要的用途就是确保 Redis 集群可以将你的请求发送到正确的集群节点。

EVALSHA

用来执行被 SCRIPT LOAD 加载到缓存的命令,具体看下文的 SCRIPT LOAD 命令介绍。

SCRIPT 命令

Redis 提供了以下几个 SCRIPT 命令,用于对脚本子系统(scripting subsystem)进行控制。

SCRIPT LOAD

将脚本 script 添加到脚本缓存中,以达到重复使用,避免多次加载浪费带宽,该命令不会执行脚本。仅加载脚本缓存中。

在脚本被加入到缓存之后,会返回一个通过SHA校验返回唯一字符串标识,使用 EVALSHA 命令来执行缓存后的脚本。

  1. 127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1]}"
  2. "8e5266f6a4373624739bd44187744618bc810de3"
  3. 127.0.0.1:6379> EVALSHA 8e5266f6a4373624739bd44187744618bc810de3 1 hello
  4. 1) "hello"
SCRIPT EXISTS

以 SHA1 标识为参数,检查脚本是否存在脚本缓存里面。

这个命令可以接受一个或者多个脚本 SHA1 信息,返回一个1或者0的列表。

  1. 127.0.0.1:6379> SCRIPT EXISTS 8e5266f6a4373624739bd44187744618bc810de3 2323211
  2. 1) (integer) 1
  3. 2) (integer) 0

1 表示存在,0 表示不存在

SCRIPT FLUSH

清空 Lua 脚本缓存 Flush the Lua scripts cache,这个是清掉所有的脚本缓存。要慎重使用。

SCRIPT KILL

杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。

这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本。

  1. # 没有脚本在执行时
  2. 127.0.0.1:6379> SCRIPT KILL
  3. (error) ERR No scripts in execution right now.
  4. # 成功杀死脚本时
  5. 127.0.0.1:6379> SCRIPT KILL
  6. OK
  7. (1.10s)
  8. # 尝试杀死一个已经执行过写操作的脚本,失败
  9. 127.0.0.1:6379> SCRIPT KILL
  10. (error) ERR Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in an hard way using the SHUTDOWN NOSAVE command.
  11. (1.19s)

假如当前正在运行的脚本已经执行过写操作,那么即使执行 SCRIPT KILL ,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用 SHUTDOWN NOSAVE 命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

SCRIPT DEBUG

redis 从 v3.2.0 开始支持 Lua debugger,可以加断点、print 变量信息、调试正在执行的代码......

如何进入调试模式?

在原本执行的命令中增加 --ldb 即可进入调试模式。

栗子

  1. # redis-cli --ldb --eval ./test.lua key1 key2 , arg1 arg2 arg3
  2. Lua debugging session started, please use:
  3. quit -- End the session.
  4. restart -- Restart the script in debug mode again.
  5. help -- Show Lua script debugging commands.
  6. * Stopped at 1, stop reason = step over
  7. -> 1 local key1 = tostring(KEYS[1])

调试模式有两种,同步模式和调试模式:

1、调试模式:使用 --ldb 开启,调试模式下 Redis 会 fork 一个进程进去到隔离环境中,不会影响到 Redis 中的正常执行,同样 Redis 中正常命令的执行也不会影响到调试模式,两者相互隔离,同时调试模式下,调试脚本结束时,回滚脚本操作的所有数据更改。

2、同步模式:使用 --ldb-sync-mode 开启,同步模式下,会阻塞 Redis 中的命令,完全模拟了正常模式下的命令执行,调试命令的执行结果也会被记录。在此模式下调试会话期间,Redis 服务器将无法访问,因此需要谨慎使用。

这里简单下看下,Redis 中如何进行调试

看下 debugger 模式支持的命令

  1. lua debugger> h
  2. Redis Lua debugger help:
  3. [h]elp Show this help.
  4. [s]tep Run current line and stop again.
  5. [n]ext Alias for step.
  6. [c]continue Run till next breakpoint.
  7. [l]list List source code around current line.
  8. [l]list [line] List source code around [line].
  9. line = 0 means: current position.
  10. [l]list [line] [ctx] In this form [ctx] specifies how many lines
  11. to show before/after [line].
  12. [w]hole List all source code. Alias for 'list 1 1000000'.
  13. [p]rint Show all the local variables.
  14. [p]rint <var> Show the value of the specified variable.
  15. Can also show global vars KEYS and ARGV.
  16. [b]reak Show all breakpoints.
  17. [b]reak <line> Add a breakpoint to the specified line.
  18. [b]reak -<line> Remove breakpoint from the specified line.
  19. [b]reak 0 Remove all breakpoints.
  20. [t]race Show a backtrace.
  21. [e]eval <code> Execute some Lua code (in a different callframe).
  22. [r]edis <cmd> Execute a Redis command.
  23. [m]axlen [len] Trim logged Redis replies and Lua var dumps to len.
  24. Specifying zero as <len> means unlimited.
  25. [a]bort Stop the execution of the script. In sync
  26. mode dataset changes will be retained.
  27. Debugger functions you can call from Lua scripts:
  28. redis.debug() Produce logs in the debugger console.
  29. redis.breakpoint() Stop execution like if there was a breakpoing.
  30. in the next line of code.

这里来个简单的分析

  1. # cat test.lua
  2. local key1 = tostring(KEYS[1])
  3. local key2 = tostring(KEYS[2])
  4. local arg1 = tostring(ARGV[1])
  5. if key1 == 'test1' then
  6. return 1
  7. end
  8. if key2 == 'test2' then
  9. return 2
  10. end
  11. return arg1
  12. # 进入 debuge 模式
  13. # redis-cli --ldb --eval ./test.lua key1 key2 , arg1 arg2 arg3
  14. Lua debugging session started, please use:
  15. quit -- End the session.
  16. restart -- Restart the script in debug mode again.
  17. help -- Show Lua script debugging commands.
  18. * Stopped at 1, stop reason = step over
  19. -> 1 local key1 = tostring(KEYS[1])
  20. # 添加断点
  21. lua debugger> b 3
  22. 2 local key2 = tostring(KEYS[2])
  23. #3 local arg1 = tostring(ARGV[1])
  24. 4
  25. # 打印输入的参数 key
  26. lua debugger> p KEYS
  27. <value> {"key1"; "key2"}

为什么 Redis 中的 Lua 脚本的执行是原子性的

我们知道 Redis 中单命令的执行是原子性的,因为命令的执行都是单线程去处理的。

那么对于 Redis 中执行 Lua 脚本也是原子性的,是如何实现的呢?这里来探讨下。

Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很类似。 在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。

Redis 中执行命令需要响应的客户端状态,为了执行 Lua 脚本中的 Redis 命令,Redis 中专门创建了一个伪客户端,由这个客户端处理 Lua 脚本中包含的 Redis 命令。

Redis 从始到终都只是创建了一个 Lua 环境,以及一个 Lua_client ,这就意味着 Redis 服务器端同一时刻只能处理一个脚本。

总结下就是:Redis 执行 Lua 脚本时可以简单的认为仅仅只是把命令打包执行了,命令还是依次执行的,只不过在 Lua 脚本执行时是阻塞的,避免了其他指令的干扰。

这里看下伪客户端如何处理命令的

1、Lua 环境将 redis.call 函数或者 redis.pcall 函数需要执行的命令传递给伪客户端;

2、伪客户端将想要执行的命令传送给命令执行器;

3、命令执行器执行对应的命令,并且返回给命令的结果给伪客户端;

4、伪客户端收到命令执行的返回信息,将结果返回给 Lua 环境;

5、Lua 环境收到命令的执行结果,将结果返回给 redis.call 函数或者 redis.pcall 函数;

6、接收到结果的 redis.call 函数或者 redis.pcall 函数会将结果作为函数的返回值返回脚本中的调用者。

这里看下里面核心 EVAL 的实现

  1. // https://github.com/redis/redis/blob/7.0/src/eval.c#L498
  2. void evalCommand(client *c) {
  3. replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
  4. if (!(c->flags & CLIENT_LUA_DEBUG))
  5. evalGenericCommand(c,0);
  6. else
  7. evalGenericCommandWithDebugging(c,0);
  8. }
  9. // https://github.com/redis/redis/blob/7.0/src/eval.c#L417
  10. void evalGenericCommand(client *c, int evalsha) {
  11. lua_State *lua = lctx.lua;
  12. char funcname[43];
  13. long long numkeys;
  14. // 获取输入键的数量
  15. if (getLongLongFromObjectOrReply(c,c->argv[2],&numkeys,NULL) != C_OK)
  16. return;
  17. // 对键的正确性做一个快速检查
  18. if (numkeys > (c->argc - 3)) {
  19. addReplyError(c,"Number of keys can't be greater than number of args");
  20. return;
  21. } else if (numkeys < 0) {
  22. addReplyError(c,"Number of keys can't be negative");
  23. return;
  24. }
  25. /* We obtain the script SHA1, then check if this function is already
  26. * defined into the Lua state */
  27. // 组合出函数的名字,例如 f_282297a0228f48cd3fc6a55de6316f31422f5d17
  28. funcname[0] = 'f';
  29. funcname[1] = '_';
  30. if (!evalsha) {
  31. /* Hash the code if this is an EVAL call */
  32. sha1hex(funcname+2,c->argv[1]->ptr,sdslen(c->argv[1]->ptr));
  33. } else {
  34. /* We already have the SHA if it is an EVALSHA */
  35. int j;
  36. char *sha = c->argv[1]->ptr;
  37. /* Convert to lowercase. We don't use tolower since the function
  38. * managed to always show up in the profiler output consuming
  39. * a non trivial amount of time. */
  40. for (j = 0; j < 40; j++)
  41. funcname[j+2] = (sha[j] >= 'A' && sha[j] <= 'Z') ?
  42. sha[j]+('a'-'A') : sha[j];
  43. funcname[42] = '\0';
  44. }
  45. /* Push the pcall error handler function on the stack. */
  46. lua_getglobal(lua, "__redis__err__handler");
  47. // 根据函数名,在 Lua 环境中检查函数是否已经定义
  48. lua_getfield(lua, LUA_REGISTRYINDEX, funcname);
  49. // 如果没有找到对应的函数
  50. if (lua_isnil(lua,-1)) {
  51. lua_pop(lua,1); /* remove the nil from the stack */
  52. // 如果执行的是 EVALSHA ,返回脚本未找到错误
  53. if (evalsha) {
  54. lua_pop(lua,1); /* remove the error handler from the stack. */
  55. addReplyErrorObject(c, shared.noscripterr);
  56. return;
  57. }
  58. // 如果执行的是 EVAL ,那么创建新函数,然后将代码添加到脚本字典中
  59. if (luaCreateFunction(c,c->argv[1]) == NULL) {
  60. lua_pop(lua,1); /* remove the error handler from the stack. */
  61. /* The error is sent to the client by luaCreateFunction()
  62. * itself when it returns NULL. */
  63. return;
  64. }
  65. /* Now the following is guaranteed to return non nil */
  66. lua_getfield(lua, LUA_REGISTRYINDEX, funcname);
  67. serverAssert(!lua_isnil(lua,-1));
  68. }
  69. char *lua_cur_script = funcname + 2;
  70. dictEntry *de = dictFind(lctx.lua_scripts, lua_cur_script);
  71. luaScript *l = dictGetVal(de);
  72. int ro = c->cmd->proc == evalRoCommand || c->cmd->proc == evalShaRoCommand;
  73. scriptRunCtx rctx;
  74. // 通过函数 scriptPrepareForRun 初始化对象 scriptRunCtx
  75. if (scriptPrepareForRun(&rctx, lctx.lua_client, c, lua_cur_script, l->flags, ro) != C_OK) {
  76. lua_pop(lua,2); /* Remove the function and error handler. */
  77. return;
  78. }
  79. rctx.flags |= SCRIPT_EVAL_MODE; /* mark the current run as EVAL (as opposed to FCALL) so we'll
  80. get appropriate error messages and logs */
  81. // 执行Lua 脚本
  82. luaCallFunction(&rctx, lua, c->argv+3, numkeys, c->argv+3+numkeys, c->argc-3-numkeys, ldb.active);
  83. lua_pop(lua,1); /* Remove the error handler. */
  84. scriptResetRun(&rctx);
  85. }
  86. // https://github.com/redis/redis/blob/7.0/src/script_lua.c#L1583
  87. void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled) {
  88. client* c = run_ctx->original_client;
  89. int delhook = 0;
  90. /* We must set it before we set the Lua hook, theoretically the
  91. * Lua hook might be called wheneven we run any Lua instruction
  92. * such as 'luaSetGlobalArray' and we want the run_ctx to be available
  93. * each time the Lua hook is invoked. */
  94. luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, run_ctx);
  95. if (server.busy_reply_threshold > 0 && !debug_enabled) {
  96. lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
  97. delhook = 1;
  98. } else if (debug_enabled) {
  99. lua_sethook(lua,luaLdbLineHook,LUA_MASKLINE|LUA_MASKCOUNT,100000);
  100. delhook = 1;
  101. }
  102. /* Populate the argv and keys table accordingly to the arguments that
  103. * EVAL received. */
  104. // 根据EVAL接收到的参数填充 argv 和 keys table
  105. luaCreateArray(lua,keys,nkeys);
  106. /* On eval, keys and arguments are globals. */
  107. if (run_ctx->flags & SCRIPT_EVAL_MODE){
  108. /* open global protection to set KEYS */
  109. lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
  110. lua_setglobal(lua,"KEYS");
  111. lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
  112. }
  113. luaCreateArray(lua,args,nargs);
  114. if (run_ctx->flags & SCRIPT_EVAL_MODE){
  115. /* open global protection to set ARGV */
  116. lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
  117. lua_setglobal(lua,"ARGV");
  118. lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
  119. }
  120. /* At this point whether this script was never seen before or if it was
  121. * already defined, we can call it.
  122. * On eval mode, we have zero arguments and expect a single return value.
  123. * In addition the error handler is located on position -2 on the Lua stack.
  124. * On function mode, we pass 2 arguments (the keys and args tables),
  125. * and the error handler is located on position -4 (stack: error_handler, callback, keys, args) */
  126. // 调用执行函数
  127. // 这里会有两种情况
  128. // 1、没有参数,只有一个返回值
  129. // 2、函数模式,有两个参数
  130. int err;
  131. // 使用lua_pcall执行lua代码
  132. if (run_ctx->flags & SCRIPT_EVAL_MODE) {
  133. err = lua_pcall(lua,0,1,-2);
  134. } else {
  135. err = lua_pcall(lua,2,1,-4);
  136. }
  137. /* Call the Lua garbage collector from time to time to avoid a
  138. * full cycle performed by Lua, which adds too latency.
  139. *
  140. * The call is performed every LUA_GC_CYCLE_PERIOD executed commands
  141. * (and for LUA_GC_CYCLE_PERIOD collection steps) because calling it
  142. * for every command uses too much CPU. */
  143. #define LUA_GC_CYCLE_PERIOD 50
  144. {
  145. static long gc_count = 0;
  146. gc_count++;
  147. if (gc_count == LUA_GC_CYCLE_PERIOD) {
  148. lua_gc(lua,LUA_GCSTEP,LUA_GC_CYCLE_PERIOD);
  149. gc_count = 0;
  150. }
  151. }
  152. // 检查脚本是否出错
  153. if (err) {
  154. /* Error object is a table of the following format:
  155. * {err='<error msg>', source='<source file>', line=<line>}
  156. * We can construct the error message from this information */
  157. if (!lua_istable(lua, -1)) {
  158. /* Should not happened, and we should considered assert it */
  159. addReplyErrorFormat(c,"Error running script (call to %s)\n", run_ctx->funcname);
  160. } else {
  161. errorInfo err_info = {0};
  162. sds final_msg = sdsempty();
  163. luaExtractErrorInformation(lua, &err_info);
  164. final_msg = sdscatfmt(final_msg, "-%s",
  165. err_info.msg);
  166. if (err_info.line && err_info.source) {
  167. final_msg = sdscatfmt(final_msg, " script: %s, on %s:%s.",
  168. run_ctx->funcname,
  169. err_info.source,
  170. err_info.line);
  171. }
  172. addReplyErrorSdsEx(c, final_msg, err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE : 0);
  173. luaErrorInformationDiscard(&err_info);
  174. }
  175. lua_pop(lua,1); /* Consume the Lua error */
  176. } else {
  177. // 将 Lua 函数执行所得的结果转换成 Redis 回复,然后传给调用者客户端
  178. luaReplyToRedisReply(c, run_ctx->c, lua); /* Convert and consume the reply. */
  179. }
  180. /* Perform some cleanup that we need to do both on error and success. */
  181. if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */
  182. /* remove run_ctx from registry, its only applicable for the current script. */
  183. luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, NULL);
  184. }

这里总结下 EVAL 函数的执行中几个重要的操作流程

1、将 EVAL 命令中输入的 KEYS 参数和 ARGV 参数以全局数组的方式传入到 Lua 环境中。

2、为 Lua 环境装载超时钩子,保证在脚本执行出现超时时可以杀死脚本,或者停止 Redis 服务器。

3、执行脚本对应的 Lua 函数。

4、对 Lua 环境进行一次单步的渐进式 GC 。

5、执行清理操作:清除钩子;清除指向调用者客户端的指针;等等。

6、将 Lua 函数执行所得的结果转换成 Redis 回复,然后传给调用者客户端。

上面可以看到 lua 中的脚本是由 lua_pcall 进行调用的,如果一个 lua 脚本中有多个 redis.call 调用或者 redis.pcall 调用的请求命令,又是如何处理的呢,这里来分析下

  1. /* redis.call() */
  2. static int luaRedisCallCommand(lua_State *lua) {
  3. return luaRedisGenericCommand(lua,1);
  4. }
  5. /* redis.pcall() */
  6. static int luaRedisPCallCommand(lua_State *lua) {
  7. return luaRedisGenericCommand(lua,0);
  8. }
  9. // https://github.com/redis/redis/blob/7.0/src/script_lua.c#L838
  10. static int luaRedisGenericCommand(lua_State *lua, int raise_error) {
  11. int j;
  12. scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
  13. if (!rctx) {
  14. luaPushError(lua, "redis.call/pcall can only be called inside a script invocation");
  15. return luaError(lua);
  16. }
  17. sds err = NULL;
  18. client* c = rctx->c;
  19. sds reply;
  20. // 处理请求的参数
  21. int argc;
  22. robj **argv = luaArgsToRedisArgv(lua, &argc);
  23. if (argv == NULL) {
  24. return raise_error ? luaError(lua) : 1;
  25. }
  26. static int inuse = 0; /* Recursive calls detection. */
  27. ...
  28. /* Log the command if debugging is active. */
  29. if (ldbIsEnabled()) {
  30. sds cmdlog = sdsnew("<redis>");
  31. for (j = 0; j < c->argc; j++) {
  32. if (j == 10) {
  33. cmdlog = sdscatprintf(cmdlog," ... (%d more)",
  34. c->argc-j-1);
  35. break;
  36. } else {
  37. cmdlog = sdscatlen(cmdlog," ",1);
  38. cmdlog = sdscatsds(cmdlog,c->argv[j]->ptr);
  39. }
  40. }
  41. ldbLog(cmdlog);
  42. }
  43. // 执行 redis 中的命令
  44. scriptCall(rctx, argv, argc, &err);
  45. if (err) {
  46. luaPushError(lua, err);
  47. sdsfree(err);
  48. /* push a field indicate to ignore updating the stats on this error
  49. * because it was already updated when executing the command. */
  50. lua_pushstring(lua,"ignore_error_stats_update");
  51. lua_pushboolean(lua, true);
  52. lua_settable(lua,-3);
  53. goto cleanup;
  54. }
  55. /* Convert the result of the Redis command into a suitable Lua type.
  56. * The first thing we need is to create a single string from the client
  57. * output buffers. */
  58. // 将返回值转换成 lua 类型
  59. // 在客户端的输出缓冲区创建一个字符串
  60. if (listLength(c->reply) == 0 && (size_t)c->bufpos < c->buf_usable_size) {
  61. /* This is a fast path for the common case of a reply inside the
  62. * client static buffer. Don't create an SDS string but just use
  63. * the client buffer directly. */
  64. c->buf[c->bufpos] = '\0';
  65. reply = c->buf;
  66. c->bufpos = 0;
  67. } else {
  68. reply = sdsnewlen(c->buf,c->bufpos);
  69. c->bufpos = 0;
  70. while(listLength(c->reply)) {
  71. clientReplyBlock *o = listNodeValue(listFirst(c->reply));
  72. reply = sdscatlen(reply,o->buf,o->used);
  73. listDelNode(c->reply,listFirst(c->reply));
  74. }
  75. }
  76. if (raise_error && reply[0] != '-') raise_error = 0;
  77. // 将回复转换为 Lua 值,
  78. redisProtocolToLuaType(lua,reply);
  79. /* If the debugger is active, log the reply from Redis. */
  80. if (ldbIsEnabled())
  81. ldbLogRedisReply(reply);
  82. if (reply != c->buf) sdsfree(reply);
  83. c->reply_bytes = 0;
  84. cleanup:
  85. /* Clean up. Command code may have changed argv/argc so we use the
  86. * argv/argc of the client instead of the local variables. */
  87. freeClientArgv(c);
  88. c->user = NULL;
  89. inuse--;
  90. if (raise_error) {
  91. /* If we are here we should have an error in the stack, in the
  92. * form of a table with an "err" field. Extract the string to
  93. * return the plain error. */
  94. return luaError(lua);
  95. }
  96. return 1;
  97. }
  98. // https://github.com/redis/redis/blob/7.0/src/script.c#L492
  99. // 调用Redis命令。并且写回结果到运行的ctx客户端,
  100. void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) {
  101. client *c = run_ctx->c;
  102. // 设置伪客户端执行命令
  103. c->argv = argv;
  104. c->argc = argc;
  105. c->user = run_ctx->original_client->user;
  106. /* Process module hooks */
  107. // 处理 hooks 模块
  108. moduleCallCommandFilters(c);
  109. argv = c->argv;
  110. argc = c->argc;
  111. // 查找命令的实现函数
  112. struct redisCommand *cmd = lookupCommand(argv, argc);
  113. c->cmd = c->lastcmd = c->realcmd = cmd;
  114. ...
  115. int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
  116. if (run_ctx->repl_flags & PROPAGATE_AOF) {
  117. call_flags |= CMD_CALL_PROPAGATE_AOF;
  118. }
  119. if (run_ctx->repl_flags & PROPAGATE_REPL) {
  120. call_flags |= CMD_CALL_PROPAGATE_REPL;
  121. }
  122. // 执行命令
  123. call(c, call_flags);
  124. serverAssert((c->flags & CLIENT_BLOCKED) == 0);
  125. return;
  126. error:
  127. afterErrorReply(c, *err, sdslen(*err), 0);
  128. incrCommandStatsOnError(cmd, ERROR_COMMAND_REJECTED);
  129. }

luaRedisGenericCommand 函数处理的大致流程

1、检查执行的环境以及参数;

2、执行命令;

3、将命令的返回值从 Redis 类型转换成 Lua 类型,回复给 Lua 环境;

4、环境的清理。

看下总体的命令处理过程

当然图中的这个栗子,incr 命令已经能够返回当前 key 的值,后面又加了个 get 仅仅是为了,演示 Lua 脚本中多个 redis.call 的调用逻辑

Redis 中 Lua 脚本的使用

限流是是我们在业务开发中经常遇到的场景,这里使用 Redis 中的 Lua 脚本实现了一个简单的限流组件,具体细节可参见

redis 实现 rate-limit

总结

当 Redis 中如果存在 读取-修改-写回 这种场景,我们就无法保证命令执行的原子性了;

Redis 在 2.6 版本推出了 Lua 脚本功能。

引入 Lua 脚本的优点:

1、减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。

2、原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。

3、复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很类似。 在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。

Redis 中执行命令需要响应的客户端状态,为了执行 Lua 脚本中的 Redis 命令,Redis 中专门创建了一个伪客户端,由这个客户端处理 Lua 脚本中包含的 Redis 命令。

Redis 从始到终都只是创建了一个 Lua 环境,以及一个 Lua_client ,这就意味着 Redis 服务器端同一时刻只能处理一个脚本。

总结下就是:Redis 执行 Lua 脚本时可以简单的认为仅仅只是把命令打包执行了,命令还是依次执行的,只不过在 Lua 脚本执行时是阻塞的,避免了其他指令的干扰。

参考

【Redis核心技术与实战】https://time.geekbang.org/column/intro/100056701

【Redis设计与实现】https://book.douban.com/subject/25900156/

【EVAL简介】http://www.redis.cn/commands/eval.html

【Redis学习笔记】https://github.com/boilingfrog/Go-POINT/tree/master/redis

【Redis Lua脚本调试器】http://www.redis.cn/topics/ldb.html

【redis中Lua脚本的使用】https://boilingfrog.github.io/2022/06/06/Redis中的原子操作(2)-redis中使用Lua脚本保证命令原子性/

Redis中的原子操作(2)-redis中使用Lua脚本保证命令原子性的更多相关文章

  1. Redis 中的原子操作(1)-Redis 中命令的原子性

    Redis 如何应对并发访问 Redis 中处理并发的方案 原子性 Redis 的编程模型 Unix 中的 I/O 模型 thread-based architecture(基于线程的架构) even ...

  2. 新姿势!Redis中调用Lua脚本以实现原子性操作

    背景:有一服务提供者Leader,有多个消息订阅者Workers.Leader是一个排队程序,维护了一个用户队列,当某个资源空闲下来并被分配至队列中的用户时,Leader会向订阅者推送消息(消息带有唯 ...

  3. Redis学习笔记六:独立功能之 Lua 脚本

    Redis 2.6 开始支持 Lua 脚本,通过在服务器环境嵌入 Lua 环境,Redis 客户端中可以原子地执行多个 Redis 命令. 使用 eval 命令可以直接对输入的脚本求值: 127.0. ...

  4. Lua脚本在Redis事务中的应用实践

    使用过Redis事务的应该清楚,Redis事务实现是通过打包多条命令,单独的隔离操作,事务中的所有命令都会按顺序地执行.事务在执行的过程中,不会被其他客户端发送来的命令请求所打断.事务中的命令要么全部 ...

  5. 【并发编程】Java中的原子操作

    什么是原子操作 原子操作是指一个或者多个不可再分割的操作.这些操作的执行顺序不能被打乱,这些步骤也不可以被切割而只执行其中的一部分(不可中断性).举个列子: //就是一个原子操作 int i = 1; ...

  6. redis --- lua 脚本实现原子操作

    如题, 楼主的想法很简单, lua 脚本本身支持原子性, 所以把命令写进一个脚本就行, 当然后续还会优化才能放到生产上,例如缓存脚本 ,redis 本身会缓存执行过的脚本 ,这样速度更快, 再优化, ...

  7. 基于Lua脚本解决实时数据处理流程中的关键问题

    摘要 在处理实时数据的过程中需要缓存的参与,由于在更新实时数据时并发处理的特点,因此在更新实时数据时经常产生新老数据相互覆盖的情况,针对这个情况调查了Redis事务和Lua脚本后,发现Redis事务并 ...

  8. Redis结合Lua脚本实现高并发原子性操作

    从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚 ...

  9. Redis进阶应用:Redis+Lua脚本实现复合操作

    一.引言 Redis是高性能的key-value数据库,在很大程度克服了memcached这类key/value存储的不足,在部分场景下,是对关系数据库的良好补充.得益于超高性能和丰富的数据结构,Re ...

随机推荐

  1. SpringBoot利用自定义注解实现通用的JWT校验方案

    利用注解开发一个通用的JWT前置校验功能 设计的预期: 系统中并不是所有的应用都需要JWT前置校验,这就需要额外设计一个注解Annotation来标识这个方法需要JWT前置校验.例如: @GetMap ...

  2. 文件上传——IIS6.0解析漏洞

    介绍 IIS6.0漏洞可分为目录漏洞和文件漏洞 目录漏洞 访问*.asp格式命令的文件夹下的文件,都会被当成asp文件执行 文件漏洞 畸形文件命名 123.asp -> 123.asp;.txt ...

  3. Linux磁盘分区fdisk命令操作(简洁版)

    实例(环境为: CentOS Linux release 7.2.1511 (Core), 3.10.0-327.el7.x86_64) 选择要具体操作的第二块磁盘(linux下一切是文件形式对应): ...

  4. Blazor 生命周期

    执行周期 1. SetParametersAsync 2. OnInitializedAsync(调用两次) 和 OnInitialized: 3. OnParametersSetAsync 或 On ...

  5. 内存之旅——如何提升CMA利用率?

    ​(以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点)​ 宋远征 李佳伟 OpenAtom OpenHarmony(以下简称"OpenHarmony") ...

  6. 安全开发运维必备,如何进行Nginx代理Web服务器性能优化与安全加固配置,看这篇指南就够了

    本章目录 1.引言 1.1 目的 1.2 目标范围 1.3 读者对象 2.参考说明 2.1 帮助参考 2.2 参数说明 3.3 模块说明 3.服务优化 3.1 系统内核 3.2 编译优化 3.3 性能 ...

  7. Ubuntu 下 firebird 数据库的安装和配置

    Ubuntu 下 firebird 数据库的安装和配置 1.简介 本文主要是 Ubuntu 下 firebird 数据库的安装和目录迁移,同样适用于 Debian 系统:Ubuntu 20.0.4 f ...

  8. Git批量下载MODIS数据

    1.download.sh获取 EarthData(需注册账号)中获取MODIS的产品类型.地理范围.时间年份等,进入下载页面Download Status 下载点击得到_download.sh 文件 ...

  9. ucore lab7 同步互斥机制 学习笔记

    管程的设计实在是精妙,初看的时候觉得非常奇怪,这混乱的进程切换怎么能保证同一时刻只有一个进程访问管程?理清之后大为赞叹,函数中途把前一个进程唤醒后立刻把自己挂起,完美切换.后一个进程又在巧妙的时机将自 ...

  10. 题解0011:图书管理(哈希、vector)

    信奥一本通--哈希 里的例题2 题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1456 题目描述:两个命令,一个是进一本名字为s的图书,一个是 ...