当前位置: 首页 > news >正文

redis源码系列--(二)--multi/exec/eval命令执行流程

本文主要记录multi/exec、eval、redis执行lua脚本的源码流程

redis在exec之前,所有queued的命令是没有执行的,!!!在执行时会通过检测client是否被打上CLIENT_DIRTY_CAS标记来判断[watch后,exec时]时间段内是否有key被修改,如果有,因为此时还没有执行事务中的任何命令,所以是可以不执行任何命令就取消整个事务的(如果是执行过程中有个命令执行是失败,此时已经进入事务执行了,所以即使当前命令失败,redis也会执行完所有的命令,即redis事务要么一条都不执行,要么就全部执行,因为redis是串行执行所有client的,所以redis可以执行事务的所有命令,而不会被其他任何client的命令中断,这也是redis事务原子性的原理)

unwatchAllKeys的实现原理(八九不离十):有一个keylist存放所有的key,即每个key都有一个watch-client-list,表示所有watch这个key的client,然后client watch这个key时,就在这个key的watch-client-list中插入一个节点,节点的值为当前client,并把这个节点的地址保存到client中,这样unwatchAllKeys中先通过client->watch_keys获取所有client watch的key,然后通过这个key节点,然后再通过这个key节点中保存的地址来获取这个key的watch_client_list,client找到watch-client-list后,然后删除就是先从keylist中摘去本节点,然后再client->watch_keys中摘除这个节点。核心就是client保存了一个指向key对应的节点的指针,即redis保存key的时候不是仅仅保存一个key,而是保存了一个附加了大量其他状态的节点,然后其他地方就直接保留一个指向该节点的指针,这样可以非常方便的通过一个指针找到整个链表或者链表中的某个节点,进而找到这个链表的其他部分,即数据只存一份,其他地方保存一个指向这份数据的指针,可以看unwatchAllKeys函数的实现,很好的体现了这一点。

如果当前客户端的ID是CLIENT_ID_AOF (即这个客户端是在处理AOF的虚拟客户端),那么Redis会执行call(c, CMD_CALL_NONE),即调用call()时,不进行任何统计、传播或者慢日志记录,只是执行命令。这是因为AOF重放期间不需要记录慢日志或传播到其他从节点。在AOF重写或加载期间,Redis只是想尽快恢复数据集,并不需要执行额外的操作(如更新慢日志、传播命令等)。因此,通过CLIENT_ID_AOF来识别AOF客户端,并简化命令的处理逻辑。如果客户端不是CLIENT_ID_AOF ,即普通客户端,Redis会使用CMD_CALL_FULL来执行命令。这意味着命令执行后会更新慢日志、统计信息,并且如果需要,还会进行AOF或从节点的传播。

!!!watch、multi/exec、eval:redis中eval lua脚本的实现和redis中exec命令的实现原理是一致的,只不过区别在于,multi/exec中,redis在开启multi之后会把exec之前的所有命令都放到命令队列里,直到遇到exec命令才会开始把这期间存储的命令一条一条执行,即multi/exec有一个命令收集的过程,因为有一个收集的过程,而收集过程必定不是原子的,也就是说在multi开始后exec执行时一个key在此期间是有可能被其他client修改的,所以multi/exec事务过程中才需要watch命令,watch是专门与multi搭配的,即在watch后exec之前key被其他client修改了,那么就取消事务,先watch,然后multi,最后exec,注意一旦watch之后,就会开启检测,也就是说watch检测的是watch之后到exec之前所有对watch_key的修改,在此期间一旦有任何其他client修改了key,那么redis都会给client打上CLIENT_DIRTY_CAS标记,也就是说[watch之后,multi之前],任何对key的修改也会取消【multi后,exec时]之间命令的执行,即会取消整个事务,因为redis执行exec时会检测client是否有CLIENT_DIRTY_CAS标记,如果有,就取消整个事务,但是不会影响[watch之后,multi之前]区间内命令的执行,因为这些命令都是当做普通命令执行,执行时不会检测client是否被打上了CLIENT_DIRTY_CAS标记,相比之下eval命令中却是一次性提交整个脚本,也就是一次性把整个事务过程中所需要的命令都提交给redis,这样就不存在提交命令过程中key被其他client修改,所以eval之前是不需要watch的,eval执行时也不会检测client是否打上了CLIENT_DIRTY_CAS标记,也就是说watch只是一个打标记的功能,至于打好标记以后怎么办则取决于命令的执行函数,如果该函数直接忽略这个CLIENT_DIRTY_CAS标记,那么watch就对他没有任何影响。!!!注意,exec执行完毕后会unwatchall,也就是取消监视所有key,也就是说执行下一次multi/exec之前如果需要watch某些key,必须重新使用watch

!!!redis lua脚本原子执行原理:redis启动的时候就会启动一个lua虚拟机,用lua变量代表,因为lua库对外提供了api,可以让外部调用者向其注册函数,也就是说外部调用者可以像lua虚拟机注册一个函数x,然后这个lua函数x对应调用者的函数y,举个例子:redis会向这个虚拟机注册(“call”,luaRedisCallCommand)函数,这表示lua中新增了一个函数call,这个函数对应的是外部的luaRedisCallCommand函数,这样lua脚本调用redis.call,那么lua虚拟机就会跳转到redis的luaRedisCallCommand,执行eval命令的时候直接把脚本提交给这个lua虚拟机就行了,lua虚拟机是单线程运行的,且直接在主线程中运行,而不是单独起一个线程。redis lua脚本的执行过程和redis exec命令的执行原理本质上是一样的,也是一条一条执行,因为redis是单线程顺序执行的,所以和事务一样,能保证原子性。执行eval时会直接把lua脚本提交给lua虚拟机来解释执行(!!!lua虚拟机是单线程,而且直接在redis主线程里运行,并不是开一个新的线程来运行lua虚拟机),lua虚拟机肯定是按脚本一行一行来执行的,这每一行都是一个get/set命令,这里一行一行执行就相当于exec里一条一条的执行命令队列中的命令,所以说lua脚本原子性的实现本质和multi/exec是一样的。redis会向这个lua虚拟机注册一系列函数,比如上面说的call,这些redis注册的lua api函数可供lua脚本调用,这些是redis环境特有的,如果lua脚本在非redis环境调用,是会报错的,lua虚拟机中调用这些函数的时候,程序控制权就会转到注册的这些函数。lua脚本举例:redis.call(“SET”, “mykey”, “new_value”),redis向lua虚拟机注册了redis.call这个接口,lua脚本执行到redis.call的时候代码就会跳转到redis的luaRedisCallCommand,这样lua中一行一行执行,实际上就是一个一个的调用对应的redis c函数,每个redis命令都对应一个执行函数,所以lua脚本和redis的exec命令没有本质区别,都是一条一条执行,因为redis是单线程,所以能保证原子性,只是lua脚本提供的功能更复杂更强大。

redis命令xxx对应的函数的名字往往都是xxxCommand的格式,比如multi命令,源码中对应的执行函数就是multiCommand、eval命令对应的执行函数是evalCommand

笔记:redis/deps/lua/src/lua.h:LUA_API int (lua_pcall) (lua_State *L, int nargs, int nresults, int errfunc); LUA_API实际是extern。即第三方库头文件中导出了一个叫做lua_pcall的函数,然后redis中直接通过头文件调用这个函数,因为是extern的,就表明这是外部包,所以说编译的时候不会去编译lua_pcall代码,而是在链接的时候直接去链接第三方lua包,这也是extern的作用。代码编译后,因为lua_pcall是直接链接的第三方包(即.o文件),就是说只有lua_pcall的声明,而没有lua_pcall的源代码,所以我们lua_pcall打断点进去是没用的。但是我们提前向lua中注册了相关函数,比如这里我们就向lua注册了一个叫做redis.call的函数,绑定的是redis源码中的luaRedisCallCommand函数,这样lua_pcall执行lua脚本时,遇到redis.call函数时就会转到我们的luaRedisCallCommand函数

笔记:lua是栈式虚拟机,所以都是push/pop来传递参数和返回值

笔记(未经证实):lua脚本是如果对key做了修改,那么就无法通过script kill来杀掉脚本,即此时lua是全部执行,和exec全部执行差不多

multi命令源码流程:

server.callmulti.multiCommand          if (c->flags & CLIENT_MULTI) {           multi禁止嵌套addReplyError(c,"MULTI calls can not be nested");return;}c->flags |= CLIENT_MULTI;                #multi命令很简单,就是标记客户端进入了multi状态,然后在此状态时,一切非exec等执行事务的命令,#他都会存起来放到client.mstate.commands数组中,这就是queued的含义#!!!这里不会重置CLIENT_DIRTY_CAS,#!!!也就是说watch开启之后到multi之前任何键的修改都会给client打上CLIENT_DIRTY_CAS的标记,#!!!因为multi没有清除CLIENT_DIRTY_CAS标志,所以当exec执行时,事务会失败#!!!也就是说exec检测的是watch之后到exec之前对watch_key的修改addReply(c,shared.ok);
}

事务冲突打标记,以set为例:

server.call            1:执行命令t_string.setCommand            t_string.setGenericCommanddb.setKeyWithDictEntry   if !SETKEY_NO_SIGNAL:                   #如果设置了通知更新,就通知所有watch这个key的所有对象(包括客户端) db.signalModifiedKey                  #通知key修改了。multi.touchWatchedKey               #凡是监视这个key的client都被标记为CLIENT_DIRTY_CAS,如果这个key是在某个事务操作中,#那么CLIENT_DIRTY_CAS这就表示该CAS操作失败了即redis的watch是一种乐观锁的方式#!!!watch的时候就会注册client要watch这个key,而不是multi时注册#!!!也就是说exec检测的是watch之后到exec之前对watch_key的修改for 所有监视这个key的客户端:c->flags |= CLIENT_DIRTY_CAS    #!!!这样multi事务执行exec时就会检测到这个标记,一旦为真说明这个key被其他人修改了,那么就会取消事务

exec命令源码流程:

server.callmulti.execCommandif (!(c->flags & CLIENT_MULTI))  #如果不是multi状态,直接返回return tag=multi.isWatchedKeyExpired    #遍历client watch的所有key,看是否有key过期了for client.watch_keys:db.keyIsExpiredif (tag) {c->flags |= (CLIENT_DIRTY_CAS) #key过期和key被修改都是一样的,都一样要取消事务的执行}if (c->flags & (CLIENT_DIRTY_CAS | CLIENT_DIRTY_EXEC)):                #!!!检测CLIENT_DIRTY_CAS标记,#!!!一旦为真说明此事务watch的key有些已经被人修改了,所以需要放弃事务 #如果有key出了问题,或者命令queued的时候出错都会放弃事务networking.addReply                                                  #填充响应到发送缓冲区multi.discardTransaction                                             #取消事务:就是清空命令队列以及清空client的multi、CLIENT_DIRTY_CAS等标记multi.freeClientMultiState                                         #释放对应的资源,这里就是释放指针以及减少引用计数#redis用mstate变量即multiState来存储与multi/exec相关的资源multi.initClientMultiState                                         #重置client.mstate的各个字段,就是都设置为0/nullc->flags &= ~(CLIENT_MULTI|CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)     #清除client的multi相关标记multi.unwatchAllKeys                                               #撤销对key的watch因为multi已经结束而了  #!!!也就是说exec之后就会取消watch,#也就是说每次执行multi/exec之前都必须watch一次 #逻辑就是:#1:key是单独存储的,一个key对应一个节点,#一个key的节点内保存了一个watch-clients-key#watch-clietnts-list包含了所有watch这个key的clients#2:client有一个watch_key链表,保存了他watch的所有key#所以先通过client->watch_key找到watch的key对应的节点的指针,#然后通过这个指针获取key的watch-clients-key链表,然后从中摘去当前client#摘完以后再回过头来在client->watch_key中摘去这个keylistRewind(c->watched_keys,&li)                                  #获取client->watch_keys_listwhile ln=next(li)&&ln!=nul:                                      #遍历所有client watch的keywk = listNodeValue(ln);                                        #从client节点获取保存的watch_key指针        clients = watchedKeyGetClients(wk);                            #通过watch_key节点获取key对应的watch-client-list即clients               listUnlinkNode(clients, watchedKeyGetClientNode(wk));          #从key对应的watch-client-list中摘去当前的clientlistDelNode(c->watched_keys,ln);                               #从client的watch_key链表中摘去当前key#开始执行事务c->flags |= CLIENT_DENY_BLOCKING;                                      #redis multi/exec中禁止执行会导致阻塞的命令multi.unwatchAllKeys                                                   #开始执行,执行之前unwatch所有key以节约cpufor client->mstate.commands                                            #commands保存了事务的所有命令,这里就是遍历数组,顺序执行所有命令if (c->id == CLIENT_ID_AOF)server.call(c, CMD_CALL_NONE)elseserver.call(commands[i],CMD_CALL_FULL)       #!!!redis是串行处理所有client fd上的事件,#!!!也就是只有处理完当前client fd后才会处理下一个client fd上的事件#!!!即在执行这个for循环的时候不会并发执行任何其他client fd上的命令#!!!因为在执行这个for循环的时候,他也是一条命令接一条命令的执行#!!!执行完一条才会执行下一条,#!!!server.call是无返回值的,就是说不管当前命令执行是否成功,#!!!都会走完整个for循环即执行所有命令而不会中途返回#!!!所以说:redis的事务,要么就是一条命令也不执行(即执行前检测到冲突就取消整个事务),#!!!!要么就是全部执行,所以说可以把redis的multi/exec看成是一个原子指令#!!!redis中lua脚本是通过eval实现的,#!!!eval之所以能保证原子性的原理和multi/exec是完全一致的 #!!!redis调用lua虚拟机执行脚本,直到脚本执行完毕才会运行后面的multi.discardTransaction                       #所有命令都执行完了,即事务已经完成了,所以可以重置client的状态了,取消事务就是一个重置clint的操作#!!!也就是说exec一次就会取消watch,也就是说每次执行multi/exec之前都必须watch一次,#而不是一次watch,多次multi/exec

redis向lua中注册函数:

server.mainserver.initServereval.scriptingInitlctx.lua_scripts = dictCreate(&shaScriptObjectDictType)    #创建一个map:key=scriptSHA1,value=script,每个执行过的脚本都会存放在这里面#evalsha命令的实现基础,即根据sha就直接从这个dict找到脚本,就省去了重新编译脚本的过程script_lua.luaRegisterRedisAPIlua_pushstring(lua, "call")                              #向lua注册redis.calllua_pushcfunction(lua, luaRedisCallCommand)              #!!!将lua中redis.call和redis的luaRedisCallCommand函数绑定起来#!!!这样lua中调用redis.call的时候程序流程就会转到redis源码中的luaRedisCallCommand函数lua_pushstring(lua, "pcall")                             #向lua注册redis.pcalllua_pushcfunction(lua, luaRedisPCallCommand);            #同上,call和pcall只是对错误的处理不同,其他都一样lua_setglobal(lua,"redis")                                 #在lua中注册全局表redis,代码里会把call/pcall注册到redis表,#这样lua中要调用函数call就是redis.call形式,即redis表的call函数

lua脚本举例:

redis.call('SET', 'k1', 'v1')
local value = redis.call('GET', 'k1')
redis.call('SET', 'k2', 'v2')
return value

“busy-reply-threshold”, “lua-time-limit”,配置文件中这两个参数是一样的,只不过一个是老版本,一个是新版本,但是最终都设置的同一个变量

笔记:阻塞redis

evel流程:

eval.evalCommandeval.evalGenericCommandlua=lctx.lua            #lctx.lua就是redis中嵌入的lua解释器script_lua.luaCallFunctionif (server.busy_reply_threshold > 0 && !debug_enabled):      #!!!busy_reply_threshold>0表示设置了脚本执行的最长时间,默认5000ms#!!!如果设置为0,那么就表示不设置。#!!!注意,这个参数并不会限制脚本的执行时间,而是达到阈值后redis就会进入busy状态#!!!一旦进入busy状态,除非脚本执行结束或者客户端发来script kill#!!!或者shutdown nosave命令#!!!否则redis不会自动结束脚本执行,并且不会响应其他任何命令包括客户端的连接#!!!如果lua脚本修改了任意key,那么script kill将不起作用,#!!!此时只能执行shutdown nosave#!!!因为如果lua脚本执行时间非常长,因为redis是单线程,所以会导致redis无法响应客户端命令#!!!"busy-reply-threshold"和"lua-time-limit"这两个参数实际是对应同一个变量#!!!前者是新版本中配置项的名称,后者是旧版本redis中的配置名称lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000)     #!!!这个钩子函数非常非常非常重要,是防止redis长时间阻塞于lua脚本的关键#!!!luaMasCountHook即指定的钩子函数,#!!!LUA_MASKCOUNT表示lua虚拟机每执行一定条数的指令就调用一次这个钩子函数#!!!100000表示每次执行10w条指令就调用一次钩子函数#!!!redis的逻辑是不管你设置脚本超时时间是多久,他都是每隔10w条指令就调用一次钩子函数#!!!然后在钩子函数里检测一下是否超时,也就是说redis这个超时时间是不精确的state=script.scriptInterrupt                             #!!!这个函数非常重要#这个函数的作用是:1:如果脚本执行时间还没有超时,那么就返回继续执行脚本#2:如果脚本第一次超时,则给脚本打上SCRIPT_TIMEDOUT标记,表示超时,然后处理其他事件#注意:这个SCRIPT_TIMEOUT是所有client都可以看见的相当于给全局变量lua打上的标记#不过此时只能处理script kill/shutdown nosave命令,其他的都会被redis拒绝#3:如果已经超时,则直接处理其他事件#即第一次超时和非第一次超时相比,#第一次超时多了一个打超时标记、打印告警日志的动作、protectClient的操作if (run_ctx->flags & SCRIPT_TIMEDOUT) {                #如果脚本被打上了SCRIPT_TIMEDOUT标记,就表明脚本已经超时了#当脚本超时时就代表redis进入了busy状态,在eventloop的processCommand中#此时只能执行含有CMD_ALLOW_BUSY的命令执行(script kill/shutdown nosave)networking.processEventsWhileBlocked();              #!!!处理一下网络读写,非常重要,后续详解#!!!whileBlocked表示当前正阻塞于lua脚本执行,#为了避免redis无法响应其他客户端,所以才有这个函数#不过此时只能处理script kill强制结束脚本或者shutdown nosave强制结束redis#!!!如果lua脚本修改了任意key,那么script kill将不起作用,#!!!此时只能执行shutdown nosave,其他任何命令都会被redis拒绝return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE;   #!!!处理完后如果客户端调用了 #SCRIPT KILL命令就会打上SCRIPT_KILLED的标记}long long elapsed = monotonic.elapsedMs(run_ctx->start_time)  #计算自启动该lua脚本开始到目前开始的时间if (elapsed < server.busy_reply_threshold) {                  #!!!如果没有超时,则返回继续执行lua脚本#即lua脚本第一次执行时最长可以连续执行这么长时间#而不用执行processEventsWhileBlocked#执行processEventsWhileBlocked就相当于打断了lua脚本的连续执行#超时以后每隔10w条就要执行一次processEventsWhileBlocked#!!!注意,只是打算lua脚本的连续执行,但不会终止,#一旦processEventsWhileBlocked执行完毕又会让lua虚拟机执行10w条指令#除非processEventsWhileBlocked处理过程中client执行了script killl#否则一旦陷入死循环,这就是个无限循环,redis此时就完全阻塞了return SCRIPT_CONTINUE;}serverLog(LL_WARNING,"Slow script detected: still in execution after %lld milliseconds. "        #打印一条警告信息"You can try killing the script using the %s command. Script name is: %s.",elapsed, (run_ctx->flags & SCRIPT_EVAL_MODE) ? "SCRIPT KILL" : "FUNCTION KILL", run_ctx->funcname);script.enterScriptTimedoutMode                     #第一次超时,给脚本打上超时标记,SCRIPT_TIMEDOUT表示脚本执行时间过长run_ctx->flags |= SCRIPT_TIMEDOUT;                networking.protectClient(client *c)                #阻止该client继续发送任何命令,直到lua脚本结束或者其他客户端发送script killc->flags |= CLIENT_PROTECTED;                    #给client打上protect标记if (c->conn):connSetReadHandler(c->conn,NULL);              #!!!清除read_handler,这样redis就不会处理该client fd上的读事件connSetWriteHandler(c->conn,NULL);             #!!!清除write_handler,这样redis就不会处理该client fd上的写事件#!!!也就是说redis是单线程+顺序执行模型,#!!!顺序执行的意思是如果上一条命令没有执行完毕或者没有被取消掉#!!!那么redis保证绝不会处理客户端发来的另一条命令,#!!!在redis没有执行完当前lua脚本之前,redis绝不会执行任何客户端的命令#!!!除了script kill和shutdown nosave命令#!!!即当lua脚本时间超过阈值时甚至陷入死循环时,redis就会进入busy状态#!!!redis在busy状态时要么等待redis执行完毕,要么由另一个client发送script kill,#!!!如果是陷入死循环,那么就只能由另一个client发送script kill强制结束#!!!注意是另一个client,因为当前client已经被清除read/writehandler了#!!!而且这个其他的客户端必须在redis陷入busy状态前就已经连接了#!!!因为redis处于busy状态时,只允许客户端发送script kill/shutdown nosave 命令,#!!!如果lua脚本修改了任意key,那么script kill将不起作用,此时只能执行shutdown nosave#!!!不会接受任何新连接和处理任何其他命令networking.processEventsWhileBlocked               #第一次超时多了一个打超时标记、打印告警日志的动作、protectClient的操作#这里就是处理redis其他命令,以免redis无法响应其他客户端#不过此时只能处理script kill 命令或者shutdown nosave强制结束redisae.aeProcessEvents......eval.scriptCommandscript.scriptKillcurr_run_ctx->flags |= SCRIPT_KILLED;              #执行scriptkill时就会给luactx打上SCRIPT_KILLED标记,return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE;if state==SCRIPT_KILL:                                         #!!!当redis处于busy状态时只有script kill/shutdown nosave命令可以结束他#一旦检测到SCRIPT_KILL,就结束lua脚本执行,#这样redis就相当于执行完了这个脚本,就可以接着处理其他未处理的描述符了lua_sethook(lua, luaMaskCountHook, LUA_MASKLINE, 0)           #防止有人用while true循环+pcall阻止redis终止该lua脚本luaError(lua);luaPushError(lua,"Script killed by user with SCRIPT KILL")    #如果经过scriptInterrupt函数后lua脚本状态为KILL,说明要终止当前脚本的执行#lua是栈式虚拟机,向lua栈中压入错误,等控制器返回lua时,lua就会检测到然后结束#如果是pcall则会让用户自己去处理错误,来决定下一步是结束lua脚本还是继续往下执行lua.lua_pcall                       #lua.lua_pcall是redis引用的第三方库lua提供的api#即调用lua解释器执行lua脚本......script_lua.luaRedisCallCommand  #redis在启动时就绑定了(redis.call,luaRedisCallCommand)#这样lua执行redis.call的时候实际执行的就是luaRedisCallCommand#lua中调用一次redis.call就会执行一次luaRedisCallCommand流程#lua中调用一次redis注册的lua函数,就会执行一次对应的redis函数#!!!lua脚本肯定是串行执行的,redis exec也是for循环串行执行#!!!都是一条一条命令的执行,因为redis是单线程模型#!!!所以lua脚本和exec命令执行期间都不会有任何其他client的命令执行#!!!所以说lua脚本的执行和exec命令的原子性背后的本质是一样的#!!!只是lua能提供更复杂的逻辑和功能script_lua.luaRedisGenericCommandscript.scriptCallclient *c = run_ctx->c                        #获取clientredisCommand=lookupCommand(c->argv, c->argc)  #解析命令,最终是一个redisCommand#其中保存了相关信息比如命令对应的执行函数#lua脚本调用redis.call时会传参#这里就是把参数解析成一个redis命令#lua脚本:redis.call('SET', 'k1', 'v1')#c->argc==3#(char*)c->argv[0]->ptr=="SET"#(char*)c->argv[1]->ptr=="k1"  #(char*)c->argv[2]->ptr=="v1"server.lookupCommandLogic(server.commands,argv,argc)  #实际就是根据argv和argc查map#server.commands是一个dict即字典c->cmd = c->lastcmd = c->realcmd = redisCommandif (cmd->flags & CMD_WRITE):                           #CMD_WRITE表示这是一个write命令,会修改数据状态run_ctx->flags |= SCRIPT_WRITE_DIRTY               #给lua虚拟机打上SCRIPT_WRITE_DIRTY标记,标记数据已经修改了#在数据修改的情况下,一旦redis处于busy状态, #client只能执行shutdown nosave,redis会拒绝执行script killserver.call(c)                           #!!!这里就跳转到server.call了#下面就是redis具体命令的执行函数了,略#至此我们就捋清了redis中lua脚本的执行原理

processEventsWhileBlocked源码流程:

networking.processEventsWhileBlockediterations=4                                #只要aeProcessEvents处理的事件不为0就立即break,否则至多空转4次                                                           while (iterations--):  long long ae_events = aeProcessEvents     #!!!aeProcessEvents就是处理一次eventLoop#!!!此时相当于在主线程的eventloop中再调用一次eventloop#!!!只不过此时处于脚本timedout状态即redis处于busy状态#!!!和普通的aeProcessEvents相比,此时只能处理少数含有CMD_ALLOW_BUSY标记的命令#!!!比如script kill/shutdown now#!!!此时redis会拒绝任何新的连接1:client对应的fd上的read事件fe->rfileProc(fe->clientData)         #fe->clientData就是我们保存的conn,这里rfileProc实际是connSocketEventHandlersocket.connSocketEventHandler(conn) #当客户端发来请求时描述符可读,所以调用的是rfileProc,这里rfileProc实际是connSocketEventHandlerconnhelpers.callHandlernetworking.readQueryFromClient(conn)......server.processCommandif (isInsideYieldingLongCommand() && !(c->cmd->flags & CMD_ALLOW_BUSY)): #!!!当lua脚本超时的时候,只允许部分命令执行#!!!这些命令都必须打上CMD_ALLOW_BUSY标记#!!!凡是没有这个标记的,此时都会被拒绝#!!!此时只有script kill/shutdown nosave可以执行#!!!一个命令有哪些标记,在commands.def文件中都提前定义好了#!!!直接ctrl+f查找 CMD_ALLOW_BUSY就可以了server.rejectCommandFormat   ...如果含有CMD_ALLOW_BUSY,就继续往下执行,下面以script kill为例.........  server.calleval.scriptCommand        #redis的xxx命令对应的处理函数为xxxCommand..script kill命令中详解..if ae_events!=0:breakserver.whileBlockedCron                    #在阻塞期间(比如rdb/aof/lua脚本超时)执行一些定时任务

script kill命令:

server.calleval.scriptCommandif argc[1]=="load":                      #script load命令,略......else if argc[1]=="kill":                 #script kill命令script.scriptKillif (curr_run_ctx->flags & SCRIPT_WRITE_DIRTY):   #lua脚本执行修改redis数据的命令比如redis.call(set,k1,v1),#此时会给lua虚拟机打上SCRIPT_WRITE_DIRTY表示数据有修改#此时redis选择拒绝执行script killaddReplyError(c, "-UNKILLABLE Sorry the script already executed write ""commands against the dataset. You can either wait the ""script termination or kill the server in a hard way ""using the SHUTDOWN NOSAVE command.");returncurr_run_ctx->flags |= SCRIPT_KILLED              #script kill只是给lua打上SCRIPT_KILLED标记#当processEventsWhileBlocked执行完毕后,redis会检测到SCRIPT_KILLED标记,#所以redis就会主动停止该脚本的执行addReply(c, shared.ok);                

相关文章:

redis源码系列--(二)--multi/exec/eval命令执行流程

本文主要记录multi/exec、eval、redis执行lua脚本的源码流程 redis在exec之前&#xff0c;所有queued的命令是没有执行的&#xff0c;&#xff01;&#xff01;&#xff01;在执行时会通过检测client是否被打上CLIENT_DIRTY_CAS标记来判断[watch后,exec时]时间段内是否有key被…...

【力扣打卡系列】移动零(双指针)

坚持按题型打卡&刷&梳理力扣算法题系列&#xff0c;语言为go&#xff0c;Day19 移动零&#xff08;双指针&#xff09; 题目描述 解题思路 p和q同时从起点移动&#xff0c;p每次都&#xff0c;q仅在交换时&#xff0c;p遇到非零数时与p值交换&#xff01;&#xff01;…...

无源元器件-电容选型参数总结

🏡《总目录》 目录 1,概述2,电容选型参数2.1,电容值(Capacitance)2.2,额定电压(Rated Voltage )2.3,外观(Appearance)2.4,尺寸(Dimension)2.5,耐压(Voltage Proof)2.6,绝缘电阻(Insulation Resistance)2.7,耗散因子或耗散系数(IQ or Dissipation Facto…...

Linux下的socket编程

概述 下面是一个通用的server端程序源码&#xff0c;用于实现两个client之间的通信。 功能 1、接收user的命令cmd消息&#xff0c;并将cmd消息发送到dev&#xff1b; 2、接收dev的应答ack消息&#xff0c;并将ack消息发送到user&#xff1b; 架构实现 通过6个线程实现。 …...

【算法】Floyd多源最短路径算法

目录 一、概念 二、思路 三、代码 一、概念 在前面的学习中&#xff0c;我们已经接触了Dijkstra、Bellman-Ford等单源最短路径算法。但首先我们要知道何为单源最短路径&#xff0c;何为多源最短路径 单源最短路径&#xff1a;从图中选取一点&#xff0c;求这个点到图中其他…...

iOS SmartCodable 替换 HandyJSON 适配记录

前言 HandyJSON群里说建议不要再使用HandyJSON&#xff0c;我最终选择了SmartCodable 来替换&#xff0c;原因如下&#xff1a; 首先按照 SmartCodable 官方教程替换 大概要替换的内容如图&#xff1a; 详细的替换教程请前往&#xff1a;使用SmartCodable 平替 HandyJSON …...

使用 axios 拦截器实现请求和响应的统一处理(附常见面试题)

在现代前端开发中&#xff0c;我们经常需要向服务器发送 HTTP 请求&#xff0c;并根据响应内容做不同的处理。axios 是一个流行的 HTTP 库&#xff0c;提供了 拦截器 功能&#xff0c;可以在请求和响应阶段插入自定义逻辑&#xff0c;这使得我们在处理认证、错误提示等场景时更…...

阿里 Sentinel

1、什么是sentinel&#xff1f; sentinel顾名思义&#xff1a;卫兵&#xff1b;在Redis中叫做哨兵&#xff0c;用于监控主从切换&#xff0c;但是在微服务中叫做流量防卫兵。 Sentinel 以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定…...

【点云网络】 pointnet 和 pointnet++

这两个网络都是斯坦福大学的一个团队提出的 我先先看一下pointnet的网络架构,这个网络比较经典&#xff0c;是2016年提出的&#xff1a; PointNet 是一个专门用于点云数据处理的神经网络。它的设计目的是直接操作不规则的点云数据&#xff0c;而无需将点云数据转换为规则网格或…...

.net core mvc 控制器中页面跳转

方式一&#xff1a; 在控制器的方法内部结尾使用 return View(); 来打开与方法同名的页面&#xff0c;如&#xff1a; public ActionResult Login() { return View(); } 该写法打开 Login 页面。 方式二&#xff1a; 可以添加参数来显式地指定要跳转的页面&#xff0…...

大学适合学C语言还是Python?

在大学学习编程时&#xff0c;选择C语言还是Python&#xff0c;这主要取决于你的学习目标、专业需求以及个人兴趣。以下是对两种语言的详细比较&#xff0c;帮助你做出更明智的选择&#xff1a; C语言 优点&#xff1a; 底层编程&#xff1a;C语言是一种底层编程语言&#x…...

跳表原理课堂笔记

课程地址 跳表是一种基于随机化的有序数据结构&#xff0c;它提出是为了赋予有序单链表以 O(logn) 的快速查找和插入的能力 创建 首先在头部创建一个 sentinel 节点&#xff0c;然后在 L1 层采用“抛硬币”的方式来决定 L0 层的指针是否增长到 L1 层 例如上图中&#xff0c;L…...

Windows系统使用OpenSSL生成自签名证书

Nginx服务器添加SSL证书。 要在Windows系统的Nginx Web服务器上使用OpenSSL生成证书&#xff0c;并确保该证书能在局域网内被计算机信任&#xff0c;你可以按照以下详细步骤进行操作&#xff1a; 一、生成证书 下载并安装OpenSSL&#xff1a; 从OpenSSL的官方网站下载适用于Wi…...

定位new的表达式

这里面会涉及内存池&#xff0c;所谓的内存池就是池化技术&#xff0c;让我们使用的更加方便&#xff0c;里面有1.线存池和连接池。 如果想要高频释放内存池&#xff0c;要针对系统有个堆&#xff0c;而堆事针对我们需要的生擒一个特例&#xff0c;和我们家庭里面妈妈给爸爸的…...

矩阵特殊打印方式

小伙伴们大家好&#xff0c;好几天没更新了&#xff0c;主要有个比赛。从今天起继续给大家更新&#xff0c;今天给大家带来一种新的题型&#xff1a;矩阵特殊打印方式。 螺旋打印矩阵 解题思路 首先给大家看一下什么是螺旋方式打印&#xff1a; 就像这样一直转圈圈。 我想大多…...

OCC 拟合的平面转换为有界平面

问题&#xff1a;针对导入的部分面无法获取大小&#xff0c;同时也无法判断点是否在面上。但是OBB可以获取大小 解决方法&#xff1a;通过面拟合转换gp_Pln&#xff0c;然后获取面的内外边&#xff0c;重新修剪生成新的TopoDS_Face 疑问&#xff1a;本人对OCC中各种面的特性不…...

Nginx性能优化的几个方法

文章目录 一 Nginx 配置优化二 缓存利用三 压缩策略四 安全性优化修改配置文件修改 Nginx 源码使用第三方模块 五 监控和日志优化六 系统层面优化七 故障转移优化 小伙伴们平时使用 Nginx 是否有进行过性能优化呢&#xff1f;还是软件装好了就直接使用呢&#xff1f; 今天松哥和…...

Unity性能优化5【物理篇】

1.刚体的碰撞检测属性首选离散型 离散碰撞的缺点是小物体快速移动时&#xff0c;有丢失碰撞的风险。此下拉菜单中&#xff0c;越下面的选项碰撞检测频率越高&#xff0c;性能消耗也显著增加。因此在选择碰撞检测类型时尽量选择离散型。 2.优化碰撞矩阵 合理标记碰撞矩阵可以减…...

我的工具列表

开发工具 名称备注Visual Studio微软开发工具集Visual Studio Code代码编辑器Qt CreatorQt IDEQt Design StudioQt 界面设计器linguistQt 国际化翻译PyCharmPython IDEVMware Workstation Pro虚拟机MATLAB数据计算和仿真Keil单片机 IDENavicat Premium数据库管理MobaXterm远程…...

985研一学习日记 - 2024.11.5

一个人内耗&#xff0c;说明他活在过去&#xff1b;一个人焦虑&#xff0c;说明他活在未来。只有当一个人平静时&#xff0c;他才活在现在。 日常 1、起床6:00 2、健身1.5h 今天练了胸&#xff0c;然后跑了会步&#xff0c;又吃多了&#xff0c;明天少吃点&#xff01; 3、…...

Vue2 与 Vue3 的区别

Vue.js 作为流行的前端框架&#xff0c;已经经历了多次版本的更新迭代&#xff0c;从 Vue2 到 Vue3 的转变不仅带来了新的功能&#xff0c;也在性能、开发体验等方面作出了显著改进。无论是对于新手还是有经验的开发者&#xff0c;了解这两个版本之间的差异都至关重要。本文将讨…...

虚拟现实技术课程开发思路

文章目录 组队选题立项分工建模说明&#xff1a;场景说明&#xff1a;交互说明&#xff1a; 结语&#xff1a; 前言&#xff1a;最近学弟学妹们反馈水水老师课程开始上强度了。不仅有翻转课堂&#xff0c;还有理论课实验课都要做东西出来。听说理论课是做什么博物馆什么的&…...

triangle_area_calculators库发布

最近将在pip网站上发布triangle_area_calculators库&#xff08;我编写的python第三方库&#xff09; triangle_area_calculators库用于计算不同类型及不同已知量的三角形面积 在triangle_area_calculators库中&#xff0c;有一个名为TriangleAreaCalculators的类 可以通过f…...

ClickHouse数据库SSL配置和SSL连接测试

目录 1.Server SSL配置介绍 2.Client SSL访问配置的介绍 3.my测试环境上开启ClickHouse Server SSL配置 & 客户端SSL访问的配置流程 4.附录 1&#xff09;SSL证书的几种类型 单域名SSL证书 通配符SSL证书 多域名SSL证书 多域名通配符SSL证书 2&#xff09;单域名…...

云渲染与汽车CGI图像技术优势和劣势

在数字时代&#xff0c;云渲染技术以其独特的优势在汽车CGI图像制作中占据了重要地位。云渲染通过利用云计算的分布式处理能力&#xff0c;将渲染任务分配给云端的服务器集群进行计算&#xff0c;从而实现高效、高质量的渲染效果。 这种技术的优势主要体现在以下几个方面&#…...

信号与噪声分析——第二节:随机变量的统计特征

2.1 单个随机变量的统计特征 随机变量是什么&#xff1f; 当随机变量X的取值个数是有限个的时候&#xff0c;我们称它为离散随机变量。 当随机变量X的取值个数是无限个的时候&#xff0c;我们称它为连续随机变量。 1. 分布函数和概率密度 1.分布函数 分布函数 定义为随机变…...

PHP网络爬虫常见的反爬策略

PHP网络爬虫在抓取数据时&#xff0c;常常会遭遇各种反爬策略。这些策略是网站为了保护自身数据不被恶意爬取而设置的。以下是一些常见的PHP网络爬虫反爬策略&#xff1a; IP限制&#xff1a; 这是最常见的反爬虫技术。通过限制IP的访问&#xff0c;可以有效防止恶意的爬虫攻击…...

java java.util.Scanner设置编码

在Java中&#xff0c;可以通过设置Scanner对象的编码来读取特定编码的输入。 使用Scanner的构造方法时&#xff0c;可以传入一个InputStream对象作为参数来设置编码。例如&#xff0c;如果要设置编码为UTF-8&#xff0c;可以这样写&#xff1a; InputStream inputStream Syst…...

小菜家教平台(二):基于SpringBoot+Vue打造一站式学习管理系统

目录 前言 今日进度 详细过程 一、数据库重构 二、编写登录接口 相关知识点 前言 昨天我们重启了小菜家教平台的开发&#xff0c;创建了新项目并初步进行了配置&#xff0c;今天我们继续。大家要是有需要源码的话可以在评论区跟我说&#xff0c;博客中就不添加源码了~ 今…...

Android AndroidManifest 文件内标签及属性

以下是重新排版后的文章&#xff1a; AndroidManifest 1. <manifest> 它是AndroidManifest.xml文件的根标签&#xff0c;包含了整个应用程序的基本信息&#xff0c;如应用程序的包名、版本代码、版本名称等。所有其他标签几乎都是在manifest标签内部定义的。 示例&…...