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 脚本最常用的命令。

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开始。

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

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2],ARGV[3]}" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
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

举个栗子

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

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

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

redis.call()
redis.pcall()

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

127.0.0.1:6379> EVAL "return redis.call('SET','test')" 0
(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
127.0.0.1:6379> EVAL "return redis.pcall('SET','test')" 0
(error) @user_script: 1: Wrong number of args calling Redis command From Lua script

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

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

下面这种就是不推荐的

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

原因有下面两个

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

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

EVALSHA

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

SCRIPT 命令

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

SCRIPT LOAD

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

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

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

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

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

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

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

SCRIPT FLUSH

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

SCRIPT KILL

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

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

# 没有脚本在执行时
127.0.0.1:6379> SCRIPT KILL
(error) ERR No scripts in execution right now. # 成功杀死脚本时
127.0.0.1:6379> SCRIPT KILL
OK
(1.10s) # 尝试杀死一个已经执行过写操作的脚本,失败
127.0.0.1:6379> SCRIPT KILL
(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.
(1.19s)

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

SCRIPT DEBUG

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

如何进入调试模式?

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

栗子

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

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

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

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

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

看下 debugger 模式支持的命令

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

这里来个简单的分析

# cat test.lua
local key1 = tostring(KEYS[1])
local key2 = tostring(KEYS[2])
local arg1 = tostring(ARGV[1]) if key1 == 'test1' then
return 1
end if key2 == 'test2' then
return 2
end return arg1 # 进入 debuge 模式
# redis-cli --ldb --eval ./test.lua key1 key2 , arg1 arg2 arg3
Lua debugging session started, please use:
quit -- End the session.
restart -- Restart the script in debug mode again.
help -- Show Lua script debugging commands. * Stopped at 1, stop reason = step over
-> 1 local key1 = tostring(KEYS[1]) # 添加断点
lua debugger> b 3
2 local key2 = tostring(KEYS[2])
#3 local arg1 = tostring(ARGV[1])
4 # 打印输入的参数 key
lua debugger> p KEYS
<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 的实现

// https://github.com/redis/redis/blob/7.0/src/eval.c#L498
void evalCommand(client *c) {
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
if (!(c->flags & CLIENT_LUA_DEBUG))
evalGenericCommand(c,0);
else
evalGenericCommandWithDebugging(c,0);
} // https://github.com/redis/redis/blob/7.0/src/eval.c#L417
void evalGenericCommand(client *c, int evalsha) {
lua_State *lua = lctx.lua;
char funcname[43];
long long numkeys; // 获取输入键的数量
if (getLongLongFromObjectOrReply(c,c->argv[2],&numkeys,NULL) != C_OK)
return;
// 对键的正确性做一个快速检查
if (numkeys > (c->argc - 3)) {
addReplyError(c,"Number of keys can't be greater than number of args");
return;
} else if (numkeys < 0) {
addReplyError(c,"Number of keys can't be negative");
return;
} /* We obtain the script SHA1, then check if this function is already
* defined into the Lua state */
// 组合出函数的名字,例如 f_282297a0228f48cd3fc6a55de6316f31422f5d17
funcname[0] = 'f';
funcname[1] = '_';
if (!evalsha) {
/* Hash the code if this is an EVAL call */
sha1hex(funcname+2,c->argv[1]->ptr,sdslen(c->argv[1]->ptr));
} else {
/* We already have the SHA if it is an EVALSHA */
int j;
char *sha = c->argv[1]->ptr; /* Convert to lowercase. We don't use tolower since the function
* managed to always show up in the profiler output consuming
* a non trivial amount of time. */
for (j = 0; j < 40; j++)
funcname[j+2] = (sha[j] >= 'A' && sha[j] <= 'Z') ?
sha[j]+('a'-'A') : sha[j];
funcname[42] = '\0';
} /* Push the pcall error handler function on the stack. */
lua_getglobal(lua, "__redis__err__handler"); // 根据函数名,在 Lua 环境中检查函数是否已经定义
lua_getfield(lua, LUA_REGISTRYINDEX, funcname);
// 如果没有找到对应的函数
if (lua_isnil(lua,-1)) {
lua_pop(lua,1); /* remove the nil from the stack */
// 如果执行的是 EVALSHA ,返回脚本未找到错误
if (evalsha) {
lua_pop(lua,1); /* remove the error handler from the stack. */
addReplyErrorObject(c, shared.noscripterr);
return;
}
// 如果执行的是 EVAL ,那么创建新函数,然后将代码添加到脚本字典中
if (luaCreateFunction(c,c->argv[1]) == NULL) {
lua_pop(lua,1); /* remove the error handler from the stack. */
/* The error is sent to the client by luaCreateFunction()
* itself when it returns NULL. */
return;
}
/* Now the following is guaranteed to return non nil */
lua_getfield(lua, LUA_REGISTRYINDEX, funcname);
serverAssert(!lua_isnil(lua,-1));
} char *lua_cur_script = funcname + 2;
dictEntry *de = dictFind(lctx.lua_scripts, lua_cur_script);
luaScript *l = dictGetVal(de);
int ro = c->cmd->proc == evalRoCommand || c->cmd->proc == evalShaRoCommand; scriptRunCtx rctx;
// 通过函数 scriptPrepareForRun 初始化对象 scriptRunCtx
if (scriptPrepareForRun(&rctx, lctx.lua_client, c, lua_cur_script, l->flags, ro) != C_OK) {
lua_pop(lua,2); /* Remove the function and error handler. */
return;
}
rctx.flags |= SCRIPT_EVAL_MODE; /* mark the current run as EVAL (as opposed to FCALL) so we'll
get appropriate error messages and logs */ // 执行Lua 脚本
luaCallFunction(&rctx, lua, c->argv+3, numkeys, c->argv+3+numkeys, c->argc-3-numkeys, ldb.active);
lua_pop(lua,1); /* Remove the error handler. */
scriptResetRun(&rctx);
} // https://github.com/redis/redis/blob/7.0/src/script_lua.c#L1583
void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled) {
client* c = run_ctx->original_client;
int delhook = 0; /* We must set it before we set the Lua hook, theoretically the
* Lua hook might be called wheneven we run any Lua instruction
* such as 'luaSetGlobalArray' and we want the run_ctx to be available
* each time the Lua hook is invoked. */
luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, run_ctx); if (server.busy_reply_threshold > 0 && !debug_enabled) {
lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
delhook = 1;
} else if (debug_enabled) {
lua_sethook(lua,luaLdbLineHook,LUA_MASKLINE|LUA_MASKCOUNT,100000);
delhook = 1;
} /* Populate the argv and keys table accordingly to the arguments that
* EVAL received. */
// 根据EVAL接收到的参数填充 argv 和 keys table
luaCreateArray(lua,keys,nkeys);
/* On eval, keys and arguments are globals. */
if (run_ctx->flags & SCRIPT_EVAL_MODE){
/* open global protection to set KEYS */
lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
lua_setglobal(lua,"KEYS");
lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
}
luaCreateArray(lua,args,nargs);
if (run_ctx->flags & SCRIPT_EVAL_MODE){
/* open global protection to set ARGV */
lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
lua_setglobal(lua,"ARGV");
lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
} /* At this point whether this script was never seen before or if it was
* already defined, we can call it.
* On eval mode, we have zero arguments and expect a single return value.
* In addition the error handler is located on position -2 on the Lua stack.
* On function mode, we pass 2 arguments (the keys and args tables),
* and the error handler is located on position -4 (stack: error_handler, callback, keys, args) */
// 调用执行函数
// 这里会有两种情况
// 1、没有参数,只有一个返回值
// 2、函数模式,有两个参数
int err;
// 使用lua_pcall执行lua代码
if (run_ctx->flags & SCRIPT_EVAL_MODE) {
err = lua_pcall(lua,0,1,-2);
} else {
err = lua_pcall(lua,2,1,-4);
} /* Call the Lua garbage collector from time to time to avoid a
* full cycle performed by Lua, which adds too latency.
*
* The call is performed every LUA_GC_CYCLE_PERIOD executed commands
* (and for LUA_GC_CYCLE_PERIOD collection steps) because calling it
* for every command uses too much CPU. */
#define LUA_GC_CYCLE_PERIOD 50
{
static long gc_count = 0; gc_count++;
if (gc_count == LUA_GC_CYCLE_PERIOD) {
lua_gc(lua,LUA_GCSTEP,LUA_GC_CYCLE_PERIOD);
gc_count = 0;
}
} // 检查脚本是否出错
if (err) {
/* Error object is a table of the following format:
* {err='<error msg>', source='<source file>', line=<line>}
* We can construct the error message from this information */
if (!lua_istable(lua, -1)) {
/* Should not happened, and we should considered assert it */
addReplyErrorFormat(c,"Error running script (call to %s)\n", run_ctx->funcname);
} else {
errorInfo err_info = {0};
sds final_msg = sdsempty();
luaExtractErrorInformation(lua, &err_info);
final_msg = sdscatfmt(final_msg, "-%s",
err_info.msg);
if (err_info.line && err_info.source) {
final_msg = sdscatfmt(final_msg, " script: %s, on %s:%s.",
run_ctx->funcname,
err_info.source,
err_info.line);
}
addReplyErrorSdsEx(c, final_msg, err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE : 0);
luaErrorInformationDiscard(&err_info);
}
lua_pop(lua,1); /* Consume the Lua error */
} else {
// 将 Lua 函数执行所得的结果转换成 Redis 回复,然后传给调用者客户端
luaReplyToRedisReply(c, run_ctx->c, lua); /* Convert and consume the reply. */
} /* Perform some cleanup that we need to do both on error and success. */
if (delhook) lua_sethook(lua,NULL,0,0); /* Disable hook */ /* remove run_ctx from registry, its only applicable for the current script. */
luaSaveOnRegistry(lua, REGISTRY_RUN_CTX_NAME, NULL);
}

这里总结下 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 调用的请求命令,又是如何处理的呢,这里来分析下

/* redis.call() */
static int luaRedisCallCommand(lua_State *lua) {
return luaRedisGenericCommand(lua,1);
} /* redis.pcall() */
static int luaRedisPCallCommand(lua_State *lua) {
return luaRedisGenericCommand(lua,0);
} // https://github.com/redis/redis/blob/7.0/src/script_lua.c#L838
static int luaRedisGenericCommand(lua_State *lua, int raise_error) {
int j;
scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
if (!rctx) {
luaPushError(lua, "redis.call/pcall can only be called inside a script invocation");
return luaError(lua);
}
sds err = NULL;
client* c = rctx->c;
sds reply; // 处理请求的参数
int argc;
robj **argv = luaArgsToRedisArgv(lua, &argc);
if (argv == NULL) {
return raise_error ? luaError(lua) : 1;
} static int inuse = 0; /* Recursive calls detection. */ ... /* Log the command if debugging is active. */
if (ldbIsEnabled()) {
sds cmdlog = sdsnew("<redis>");
for (j = 0; j < c->argc; j++) {
if (j == 10) {
cmdlog = sdscatprintf(cmdlog," ... (%d more)",
c->argc-j-1);
break;
} else {
cmdlog = sdscatlen(cmdlog," ",1);
cmdlog = sdscatsds(cmdlog,c->argv[j]->ptr);
}
}
ldbLog(cmdlog);
}
// 执行 redis 中的命令
scriptCall(rctx, argv, argc, &err);
if (err) {
luaPushError(lua, err);
sdsfree(err);
/* push a field indicate to ignore updating the stats on this error
* because it was already updated when executing the command. */
lua_pushstring(lua,"ignore_error_stats_update");
lua_pushboolean(lua, true);
lua_settable(lua,-3);
goto cleanup;
} /* Convert the result of the Redis command into a suitable Lua type.
* The first thing we need is to create a single string from the client
* output buffers. */
// 将返回值转换成 lua 类型
// 在客户端的输出缓冲区创建一个字符串
if (listLength(c->reply) == 0 && (size_t)c->bufpos < c->buf_usable_size) {
/* This is a fast path for the common case of a reply inside the
* client static buffer. Don't create an SDS string but just use
* the client buffer directly. */
c->buf[c->bufpos] = '\0';
reply = c->buf;
c->bufpos = 0;
} else {
reply = sdsnewlen(c->buf,c->bufpos);
c->bufpos = 0;
while(listLength(c->reply)) {
clientReplyBlock *o = listNodeValue(listFirst(c->reply)); reply = sdscatlen(reply,o->buf,o->used);
listDelNode(c->reply,listFirst(c->reply));
}
}
if (raise_error && reply[0] != '-') raise_error = 0;
// 将回复转换为 Lua 值,
redisProtocolToLuaType(lua,reply); /* If the debugger is active, log the reply from Redis. */
if (ldbIsEnabled())
ldbLogRedisReply(reply); if (reply != c->buf) sdsfree(reply);
c->reply_bytes = 0; cleanup:
/* Clean up. Command code may have changed argv/argc so we use the
* argv/argc of the client instead of the local variables. */
freeClientArgv(c);
c->user = NULL;
inuse--; if (raise_error) {
/* If we are here we should have an error in the stack, in the
* form of a table with an "err" field. Extract the string to
* return the plain error. */
return luaError(lua);
}
return 1;
} // https://github.com/redis/redis/blob/7.0/src/script.c#L492
// 调用Redis命令。并且写回结果到运行的ctx客户端,
void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) {
client *c = run_ctx->c; // 设置伪客户端执行命令
c->argv = argv;
c->argc = argc;
c->user = run_ctx->original_client->user; /* Process module hooks */
// 处理 hooks 模块
moduleCallCommandFilters(c);
argv = c->argv;
argc = c->argc; // 查找命令的实现函数
struct redisCommand *cmd = lookupCommand(argv, argc);
c->cmd = c->lastcmd = c->realcmd = cmd; ... int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
if (run_ctx->repl_flags & PROPAGATE_AOF) {
call_flags |= CMD_CALL_PROPAGATE_AOF;
}
if (run_ctx->repl_flags & PROPAGATE_REPL) {
call_flags |= CMD_CALL_PROPAGATE_REPL;
}
// 执行命令
call(c, call_flags);
serverAssert((c->flags & CLIENT_BLOCKED) == 0);
return; error:
afterErrorReply(c, *err, sdslen(*err), 0);
incrCommandStatsOnError(cmd, ERROR_COMMAND_REJECTED);
}

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. java中什么是Interface接口, 请给个实例!

    1.Interface接口的定义和用法  先直接上大白话:马克-to-win:接口就是灰常灰常抽象的抽象类,我们可以就像用抽象类一样用接口,只不过,interface抽象到不能再抽象了,以至于里面不能 ...

  2. uniapp中生成二维码(附代码和插件)

    wxqrcode.js文件:  https://github.com/Clearlovesky/-js-jq-/tree/master/wxqrcode // 引入二维码库 import QR fro ...

  3. SpringMVC基于注解开发的步骤

    基于xml配置 .1准备好以下相关jar包 .2创建Maven项目使用骨架  (这里选择第二个以webapp结尾的非第一个) 给项目起个名字 这里可以更改maven本地仓库(依赖包所存放的地方)的路径 ...

  4. caioj 1002: [视频]实数运算2[水题]

    题意:输入三个数,计算并输出它们的平均值以及三个数的乘积,结果保留2位小数. 题解:简单题不写题解了-- 代码: #include <cstdio> double a, b, c; int ...

  5. 序列化和反序列化为什么要实现Serializable接口?(史上最全、简单易懂)

    目录结 前言 1.什么是序列化和反序列化 2.什么时候需要进行序列化和反序列化 2.1.服务器和浏览器交互时用到了Serializable接口吗? 2.2.Mybatis将数据持久化到数据库中用到了S ...

  6. Java学习——数组的基础知识

    数组的特点.分类:一维.二维数组的使用:数组的声明和初始化.调用数组的指定位置的元素.获取数组的长度.遍历数组.数组元素的默认初始化值

  7. linux目录结构知识

    1.系统目录结构介绍 1.目录结构特点 linux系统中的目录一切从根开始. Linux系统中的目录结构拥有层次. Linux系统中的目录需要挂载使用. 2.目录挂载初识 挂载的命令:mount mo ...

  8. Flex 的 多种对齐属性

    1. html 结构 <div id="container"> <div class="item item-1"> <h3> ...

  9. Ubuntu 下 Mariadb 数据库的安装和目录迁移

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

  10. Day 005:PAT练习--1047. 编程团体赛(20)

    编程团体赛的规则为:每个参赛队由若干队员组成:所有队员独立比赛:参赛队的成绩为所有队员的成绩和:成绩最高的队获胜.现给定所有队员的比赛成绩,请你编写程序找出冠军队. 输入格式: 输入第一行给出一个正整 ...