想象你家楼下有一家 24 小时便利店,柜台后面只有一个收银员老王。买瓶水,你说一句,他动一下;但要办“查会员积分→算折扣→扣库存”三件事,你得跟他来回说三句话。更糟的是,你说第二句时,别的顾客插了进来,老王就先去招呼别人——等你再开口,之前算好的价格全乱了。Redis 就像这家便利店:再快,也只有一个“收银员”。而今天要讲的 Lua 脚本,就是一张“流程便签”:你把整段流程写在便签上递给老王,他会一口气从头做到尾,中途绝不分心,你也只需要跑一趟。这就是 Redis Lua 脚本的原子性、低延迟和并发安全三件事的本质。接下来,我们从一张便签出发,一步步看懂脚本的执行原理、缓存编号、错误处理、性能优化、阻塞风险和生产落地。
掌握 Redis Lua 脚本,是后端工程师把复杂业务安全下沉到缓存层的核心技能,也是高并发扣减与限流场景的必备武器,学好它能让你的系统在并发正确性与响应速度上同时领先。
可视化演示
动态演示Redis中的Lua脚本的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Redis中的Lua脚本:Lua脚 Lua脚本原子执行原理 EVAL与EVALSHA命令及 核心机制可视化 Redis中的Lua脚本整体架构与关键组件
脚本执行原理
便利店收银员老王只会听简单的短句——“可乐多少钱”“扫码付款”。如果顾客要做“会员积分抵扣 + 满减券 + 库存扣减”三步操作,按老规矩得跟老王来回说三句话。现在店长给老王培训了一种“便签语言”:顾客把三步操作全写在一张便签上,老王照着从上到下执行一遍。这张便签,就是 Redis 里的 Lua 脚本。
在 Redis 中,客户端用 EVAL 命令把脚本提交给服务器:EVAL script numkeys key [key ...] arg [arg ...]。重点是 numkeys 之后的 key 参数被特殊标记。为什么要区分 key 和 arg?因为 Redis Cluster 靠 key 计算哈希槽,只有 key 参与路由;arg 只是普通参数。这个细节决定了脚本在集群里能不能跑,后面的生产实践会专门讲。
Redis 内置了一个 Lua 5.1 解释器,运行在服务器的“沙箱”里。脚本里不能访问文件、网络、操作系统,唯一的“对外窗口”是 redis.call 和 redis.pcall 两条命令,用来调用 Redis 自身的命令。因为 Redis 是单线程的(只有一个收银员),脚本一旦开始执行,服务器会暂时无视所有其他请求,直到脚本跑完——这正是原子性的来源。脚本的返回值会自动从 Lua 类型转换为 Redis 协议类型:整数、字符串、数组,Lua 的 nil 会变成空结果,false 会变成 0。
说白了,便签上的每一行字老王都认识,但他只在你递给他的那一刻开始念,念完之前谁的嗓门他也不理。这一“不理”,就是后面所有并发正确性的地基。
- 脚本编译Redis 收到 EVAL 后先把 Lua 源码编译为内部指令,编译失败则整个请求直接报错,一条命令都不会执行。
- 逐条执行解释器按顺序把每条命令交给 Redis 执行,期间不响应任何其他客户端。
- 结果转换最后一条表达式的值按 Redis 协议转换后返回给客户端。
原子性保证
便利店最怕的场景:货架上只剩 3 份关东煮,A 和 B 几乎同时要买。老规矩下,老王先告诉 A“还有 3 份”,A 算了一下要买 2 份,可就在 A 报出“改库存为 1”的瞬间,B 也问了“还有几份”,老王又答“3 份”。等 A 写入库存 1,B 紧跟着写入库存 2——最终货架写着 2,实际上已经卖出去 3 份,超卖了。
超卖的本质是“检查-计算-写入”三步之间留了时间窗。任何其他请求在这个窗口里插一脚,后写覆盖先写,数据就错了。在数据库里,我们用事务和锁;在 Redis 里,Lua 脚本给出了更轻的答案:把三步写进一张便签,老王拿到后一口气做完。这一刻,“读”和“写”之间不再有缝隙。
与 MULTI/EXEC 事务对比:Redis 事务也能打包命令,但打包期间不能“根据上一条结果决定下一条”——你想“先看库存再决定是否扣减”,事务做不到,必须借助 WATCH 乐观锁,冲突了还要重试。Lua 脚本则可以读改写一气呵成,天然原子,无需 WATCH。代价是:脚本出错时不会回滚(下一节详述)。并发安全应用的经典场景是扣库存、限流计数、分布式锁释放——都是“把条件判断和动作绑在一起”的需求。
把“看、想、写”压缩成一次执行,是 Lua 给并发问题最朴素的解法——不是让两个顾客排队,而是让每个顾客“一站到底”。
- 业务算式扣减库存就是算一行减法。
- 命令拆分在 Redis 中这行减法必须拆成 GET 和 SET 两条命令。
- 时间窗两条命令之间任何请求都能插进来,造成互相覆盖。
脚本缓存机制
老王记性一般,每次顾客递来一张新便签,他都要逐字读一遍。读得多了,店长想了个办法:收银台背后贴了一面“便利贴墙”,每张新便签第一次用时,老王就抄一份贴上去,并给它编个号。下次顾客再说“用 3 号流程”,老王连全文都不用看,直接照做。
在 Redis 里,每次 EVAL 执行脚本,服务器都会把脚本缓存下来,并计算一个 SHA1 摘要作为编号(就像“3 号”)。客户端只要保存这个编号,下次用 EVALSHA 命令提交编号即可,不用再传整个脚本。省下的主要是脚本内容在网络上传送的成本——对追求极低延迟的场景,每一字节都珍贵。
脚本缓存是“常驻”的:除非显式执行 SCRIPT FLUSH 或 Redis 重启,否则脚本会一直留在缓存里。既然可能失效,生产客户端必须处理 NOSCRIPT 错误:收到这个错误就改用 EVAL 重新加载脚本并重试。主流客户端(如 Redisson、Lettuce)已内置回退逻辑,自研客户端时这个细节最容易被漏掉。
便利贴墙让“熟流程”不用一遍遍重新读——这便是 EVALSHA 的智慧:把重复传输变成一次编号引用,把网络开销压到最小。
- SHA1 编号计算对脚本全文做 SHA1 散列,得到 160 位摘要作为唯一编号,内容相同则编号相同。
- 缓存判定收到 EVALSHA 时先查编号是否在缓存中。
- 命中与回退命中直接执行;未命中返回 NOSCRIPT 错误,客户端回退到 EVAL。
错误处理机制
便签也有写错的时候,分两种。第一种是“字迹根本没法认”,比如语法乱写、括号没闭合——老王接过来一看,直接退回,一个动作都没做。第二种是“念到一半发现不对”,比如第 3 步说“把牛奶价格改成 X”,可店里根本没有牛奶——这时麻烦就来了:老王已经念完了前两步,该盖的章、该改的价格都已经做了。
在 Redis 里,第一种叫编译期错误:脚本在编译阶段就失败,Redis 直接返回错误,一条命令也不执行。第二种叫运行期错误。关键认知是:Redis 的 Lua 脚本没有“回滚”机制。出错时,脚本立即终止,但错误发生之前已经执行的写命令全部保留。这和我们熟悉的数据库事务“全成或全败”完全不同。官方选择不回滚,理由是保持实现简单和高性能,同时很多错误本质上是“业务判断错”,回滚反而可能掩盖问题。
对运行期错误的处理,取决于你用哪条调用命令。redis.call 出错时抛出异常,脚本立即终止,异常消息返回给客户端;redis.pcall(protected call)出错时不抛异常,而是返回一个包含 err 字段的 Lua 表,脚本自己决定怎么处理,甚至可以继续往下执行。想让脚本“出错了也能优雅善后”,就用 pcall;想让脚本“遇到错误立刻停”,就用 call。
所以,写便签的人要对便签的“前几步”负责——Redis 不会帮你擦掉已印下的章。设计脚本时,把可能出错的敏感操作尽量放后面,出错影响面就小一些。
- call 直接终止redis.call 遇到错误时异常上抛,脚本立即中断并返回错误。
- pcall 捕获错误redis.pcall 把错误包装进结果表,脚本可读取 err 字段继续运行。
- 没有回滚出错前已执行的写命令全部保留,不存在撤销操作。
性能优化技巧
顾客从店门口走到柜台要 1 分钟,在柜台说话只要 5 秒。要办三件事,按老规矩来回跑三趟,路上花 3 分钟;写在一张便签上一趟搞定,路上只花 1 分钟。Lua 脚本性能红利的本质,就是把“多趟路费”砍成“一趟路费”——这一趟路费就是网络往返时间 RTT。
Pipeline 也能减少往返,但它缺少“中途判断”和原子性;Lua 脚本两者兼得:既要少跑路,又要中途能做决定。于是性能优化的第一个原则是:能用一条 Redis 命令,别用 Lua;发现多条命令需要原子协作,才上 Lua。第二个原则是瘦身:脚本里不要放大循环,比如对几千个 key 做 KEYS * 再逐个处理——每多一个命令,阻塞时间就线性增加。
第三个原则是参数化:把变化的值用 ARGV 传入,而不是在脚本里做字符串拼接。因为脚本是缓存的,内容固定才能稳定复用 SHA;每次拼接既浪费 CPU,又会让不同脚本填满缓存。第四个原则是避免在脚本里调用高耗时或阻塞命令(阻塞型 BLPOP 在脚本中甚至不被允许)。脚本应短小、快速、聚焦,把 Lua 当“胶水”,不当“计算引擎”。
一张便签省下的是路费,但别为了省路费把便签写成“万字长文”——老王念太久,全店的人都得等着。省路费和防阻塞,永远是同一枚硬币的两面。
- 多命令成本每条命令都要单独往返,总耗时为 n 个 RTT 加 n 次执行时间。
- 脚本打包脚本只花一次 RTT,服务器端串行执行 n 条命令。
- 收益量化两者之差是 (n-1) 个 RTT,n 越大省得越多。
阻塞与超时
最担心的事还是发生了:某张便签上写了个“从 1 数到无穷大”的循环,老王念起来就停不下。柜台前的队伍越排越长,连买瓶水的老顾客也被堵在外面——因为店里只有老王一个收银员。
在 Redis 中,脚本的无限循环或超大循环会让整个服务器“卡死”:所有读写命令都在排队,监控连不上,报警器狂响。Redis 为此提供了一道保险:配置文件里的 lua-time-limit,默认 5000 毫秒。但这个“保险”并非自动杀死脚本,而是:脚本运行超过该时长后,Redis 开始愿意响应其他客户端发来的 SCRIPT KILL 命令。
为什么不能直接杀掉?因为脚本正占着单线程的 CPU,服务器根本没有机会安全中断它,只能由另一个连接发指令请求“协商终止”。而且 SCRIPT KILL 有个硬条件:脚本还没执行过任何写命令。如果脚本已经写了数据,杀了它会导致“写了一半、无法回滚、数据不一致”的烂摊子,此时唯一的保底是 SHUTDOWN NOSAVE——放弃内存数据,重启 Redis。生产上遇过脚本卡死的老手,提到 SHUTDOWN NOSAVE 都心有余悸:那意味着缓存全部重建。
与其寄希望于善后,不如从源头防范:脚本里避免死循环(循环次数用 ARGV 参数限制),避免大 key 遍历,上线前用压测估算最坏执行耗时。便签上别写“永远念不完”的咒语,才是治本之策。
- 超时阈值脚本执行超过 lua-time-limit 毫秒后,服务端开始接收终止指令。
- 无写可杀脚本尚无写命令时,SCRIPT KILL 可安全终止,已完成的读结果作废。
- 有写只能丢已执行写命令时无法回滚,只能 SHUTDOWN NOSAVE 重启。
生产实践要点
单店(单机 Redis)验证成功的脚本玩法,要搬到连锁店(Redis Cluster)就得多守一条规矩:一个顾客必须在一家分店结账。脚本里操作的所有 key,都必须落在同一个哈希槽(slot)上,否则 Redis 会直接拒绝执行——因为它不知道把脚本发给哪家分店。
哈希槽的计算公式是 slot = CRC16(key) mod 16384。要让多个 key 同槽,最实用的是哈希标签(hash tag):把同一个变量名放进大括号,比如 {user:123}:cart 和 {user:123}:points,Redis 只对大括号内的部分做 CRC16,于是两个 key 必然同槽。集群环境还有一个部署纪律:发布前用 SCRIPT LOAD 把脚本预热进节点缓存,再让线上用 EVALSHA;若遇到 NOSCRIPT,回退 EVAL 并告警,因为“突然大量回退”可能是缓存被清空的信号。
脚本也是代码,要纳入版本管理:脚本文件入库,以 SHA 为版本标识,改动脚本内容会产出新 SHA,天然可追溯。调试用 redis-cli --ldb EVAL 可以像调试普通代码一样下断点、单步执行,排查逻辑错误。监控方面,关注 SLOWLOG 里的脚本执行耗时、Redis 的 busy 状态、客户端连接堆积;一旦出现 SCRIPT KILL 或 SHUTDOWN NOSAVE 事件,立刻告警并复盘。
从一家便利店到全国连锁,不变的是一张便签走天下的效率,变的是“分店规矩”和“发布纪律”。规矩守好了,Lua 脚本就是生产环境的稳定引擎;规矩破了,它就是最快的故障制造机。
- CRC16 散列先对 key 计算 16 位校验值,得到中间结果 h。
- 取模分槽对 16384 取模,得到 0 到 16383 的槽编号。
- 同槽约束脚本内所有 KEYS 的槽必须相同,否则集群拒绝执行。
AI 应用场景
秒杀与库存扣减
阶段:数据一致性保障(写并发控制)
解决的问题:秒杀场景下,成千上万请求同时扣库存,传统 GET 后再 SET 的读改写流程会产生超卖。AI 平台在做推荐流量的同时,后端往往还要支撑用户加购、下单等强一致写操作。用 Lua 把“检查库存 → 判断充足 → 扣减数量”打成一段原子脚本,能保证任何时刻都只有一个请求在执行扣减判定,从根上杜绝超卖。
具体案例:某电商大促时,商品详情页的 Redis 缓存中维护库存字段,下单接口调用 Lua 脚本原子扣减;脚本从 1000 件库存开始处理 5000 个并发请求,最终成功数恰好等于 1000,零超卖,响应时间稳定在个位数毫秒级。
分布式限流器
阶段:服务治理与流量控制
解决的问题:限流器常用“INCR 计数 + EXPIRE 设置有效期”实现,但这两步非原子,窗口切换时可能计数错乱,导致限流失效。Lua 脚本把“计数判断 → 自增 → 重置窗口”打包成一个原子步骤,既保证限流阈值准确,又省去多次网络交互。
具体案例:某 AI 网关对每个用户的推理请求按每秒 10 次限流,每个用户一个 Redis key,脚本用 ARGV 传入阈值与当前时间戳,在窗口内原子完成计数与判定,超限请求直接返回 429,限流误判率从 5% 降到接近 0。
分布式锁安全释放
阶段:分布式协作与互斥
解决的问题:用 Redis 实现分布式锁时,释放锁需要“先检查持有者是否是自己,再 DEL 删除”。这两步非原子的话,可能把别人刚获取的锁删掉,造成互斥失效。Lua 脚本把“GET 比对持有者 → DEL 删除”合为一句原子脚本,确保只有锁的持有者才能释放锁。
具体案例:某机器学习训练平台的多机多卡任务用分布式锁抢占 GPU 资源,节点释放锁时携带自己的唯一标识,通过 Lua 校验后删除;自研锁上线半年,未发生一次锁误删导致的双节点同时训练事故。
Python 演示
# -*- coding: utf-8 -*-"""Redis Lua 脚本演示:用一张"便签"安全扣减库存, 避免超卖。"""import redis LUA_DEDUCT = """local stock = tonumber(redis.call('GET', KEYS[1]) or '0')local need = tonumber(ARGV[1])if stock >= need then redis.call('DECRBY', KEYS[1], need) return 1endreturn 0""" def main(): client = redis.Redis(host='127.0.0.1', port=6379, db=0) client.set('stock:iphone', 10) sha = client.script_load(LUA_DEDUCT) print('脚本 SHA 编号:', sha) success = 0 for _ in range(20): result = client.evalsha(sha, 1, 'stock:iphone', 1) if result == 1: success += 1 print('成功购买次数:', success) print('剩余库存:', client.get('stock:iphone').decode()) client.script_flush() try: client.evalsha(sha, 1, 'stock:iphone', 1) except redis.exceptions.ResponseError as exc: print('缓存失效报错:', exc) client.script_load(LUA_DEDUCT) print('回退到 EVALSHA 后:', client.evalsha(sha, 1, 'stock:iphone', 1)) if __name__ == '__main__': main()
LUA_DEDUCT = """..."""便签内容:一段 Lua 源码字符串,内含“读库存→判断→扣减”逻辑。client.script_load(LUA_DEDUCT)预加载脚本拿到 SHA 编号,相当于把便签贴到便利贴墙。client.evalsha(sha, 1, 'stock:iphone', 1)只传编号与参数执行脚本,一趟网络往返完成读改写。redis.call('GET', KEYS[1])Lua 内部调用 Redis 命令,结果直接进入 Lua 变量参与判断。if stock >= need then判断在脚本内完成,整个判断到扣减的过程不会被其他请求插队。client.script_flush()模拟脚本缓存被清空,验证生产环境的 NOSCRIPT 场景。except redis.exceptions.ResponseError as exc捕获编号不存在错误,回退重新加载脚本,这是生产客户端的标准兜底。
常见误区
误区我以为 Redis 的 Lua 脚本和 MySQL 事务一样,要么全部成功,要么全部失败。脚本执行到一半报了错,之前写的那些 key 应该都会被撤销吧?
正解完全相反。Redis Lua 脚本没有回滚机制,出错时脚本立即停止,但错误前已执行的写命令全部保留。就像老王念便签念到第 3 步发现不对,但前两步已经盖了章,章不会消失。设计脚本时,尽量把容易出错的命令放后面,并多用 pcall 主动判断,减少“写了一半”的烂摊子。
为何常见:大家对数据库事务“全成或全败”的印象太深,默认 Redis 也是同样语义,而官方文档对无回滚的解释相对低调,容易忽略。
误区配置里写了 lua-time-limit 5000,那脚本运行超过 5 秒,Redis 应该会自动终止它,然后继续服务,对吧?
正解不对。超时只是让服务器从“完全不响应外部命令”变为“愿意接收 SCRIPT KILL 或 SHUTDOWN NOSAVE”。脚本本身仍然霸占着单线程继续跑,直到有人用另一个连接喊停。而且如果脚本已经写过数据,连 SCRIPT KILL 都会被拒绝,只能 SHUTDOWN NOSAVE 丢数据重启。超时是“开了一扇干预的门”,不是“自动断电的保险丝”。
为何常见:把“超时”理解为“强制执行上限”,忽略了一条重要事实——单线程里正在运行的代码根本无法被安全抢占。
误区Redis 单命令都是微秒级,那我在 Lua 脚本里循环一万次调用 HSET 也就几十毫秒,应该不影响线上吧?
正解影响巨大。Redis 是单线程,脚本循环期间全局阻塞,所有读写请求全部排队,一次几毫秒的循环在高峰期会被放大成大量请求的排队延迟,甚至触发客户端超时雪崩。性能优化的核心是“脚本瘦身”:把循环移到应用层,用原生命令批量操作,用 ARGV 传参代替脚本内拼接,让每段脚本都短小精悍。
为何常见:人们只看到单条命令快,忽略了单线程模型下“单条命令再快,也扛不住脚本内的万次循环”。
误区EVALSHA 比 EVAL 省带宽,线上全部切到 EVALSHA 后,脚本肯定在缓存里,不用再处理什么 NOSCRIPT 错误。
正解脚本缓存不是永恒的,Redis 重启、SCRIPT FLUSH 都会清空它。一旦缓存被清,EVALSHA 会返回 NOSCRIPT。生产客户端必须捕获该错误并回退到 EVAL 重新加载脚本,否则业务会大面积报错。主流客户端内置了回退,自研客户端时这几乎是必踩的坑。
为何常见:把“缓存常驻”误以为“永不移除”,又忽略了重启和显式清理这两个常见运维动作。
面试题
Q1. Redis 执行 Lua 脚本时的核心运行机制是什么?
- A.多线程并行执行提升吞吐量
- B.单线程串行执行保证原子性← 正确答案
- C.独立进程执行不影响主流程
- D.批量异步执行不保证顺序
答案:B。Redis 只有一个“收银员”,脚本运行时其他请求全部排队,这种单线程串行恰好保证了脚本的原子性。A 错在 Redis 不是多线程执行脚本;C 错在脚本就在主进程内跑;D 错在脚本是同步阻塞执行,顺序完全确定。
Q2. EVAL 命令中 numkeys 参数的作用是什么?
- A.指定脚本允许运行的最长秒数
- B.声明后续参数中 KEYS 的个数← 正确答案
- C.规定脚本最多返回几个结果值
- D.限制脚本申请内存的上限值
答案:B。numkeys 告诉 Redis 在 key 列表和 arg 列表之间划一条线:前 numkeys 个参数是 KEYS,其余是 ARGV。区分两者在集群路由和哈希槽计算中至关重要。A、C、D 都不是 numkeys 的语义。
Q3. 相比每次重传脚本,EVALSHA 的主要收益是什么?
- A.脚本可以执行得更持久
- B.只传编号省下网络带宽← 正确答案
- C.自动修复脚本里的语法错
- D.允许跨节点执行脚本
答案:B。EVALSHA 只在网络上传输脚本的 SHA1 编号,不再发送完整脚本,显著节省带宽。脚本执行时间与命令数有关,与选 EVAL 还是 EVALSHA 无关,A 错;C、D 与该特性不符。
Q4. 关于 Lua 脚本运行期错误,下列说法正确的是?
- A.出错前已执行的写命令不会回滚← 正确答案
- B.redis.pcall 能把错误变成返回值← 正确答案
- C.编译错会执行脚本的前半部分
- D.redis.call 出错会让脚本停下← 正确答案
答案:ABD。脚本没有回滚机制,错前写命令已生效,A 对;pcall 把错误包装进结果表返回,B 对;编译错误发生在执行之前,一条命令都不会执行,C 错;call 出错立即抛异常终止脚本,D 对。
Q5. 脚本运行超过 lua-time-limit 后,Redis 会怎样?
- A.立刻强制终止正在运行的脚本
- B.接受客户端的 SCRIPT KILL 请求← 正确答案
- C.自动重启脚本并清空所有数据
- D.把脚本切到后台继续执行
答案:B。超时不是自动杀进程,而是让服务器从“不理会任何命令”变为“响应终止指令”。客户端的 SCRIPT KILL 在超时前会被忽略,超时后才有机会介入。A、C、D 均与机制不符。
Q6. 脚本已执行过写命令且陷入死循环时,正确做法是?
- A.直接执行 SCRIPT KILL 等待自动回滚
- B.执行 SCRIPT FLUSH 清空所有脚本
- C.执行 SHUTDOWN NOSAVE 丢数据重启← 正确答案
- D.等待超时后 Redis 自动恢复
答案:C。写过数据后无法安全回滚,SCRIPT KILL 会被拒绝,唯一保底是 SHUTDOWN NOSAVE 放弃内存数据重启,这也是为什么脚本里的写操作要慎之又慎。A 的“自动回滚”不存在;B 的清缓存解决不了卡死;D 中 Redis 不会自动恢复。
Q7. Redis Cluster 中编写 Lua 脚本的合规做法有?
- A.确保所有 KEYS 映射到同一哈希槽← 正确答案
- B.用 {user:1}:cart 形式的哈希标签← 正确答案
- C.脚本内可任意访问各分片键值
- D.上线前用 SCRIPT LOAD 预热缓存← 正确答案
答案:ABD。集群要求脚本所有 KEYS 同槽,A 对;哈希标签强制同槽,B 对;跨分片键的脚本会被集群拒绝,C 错;预热后客户端才能放心用 EVALSHA,D 对。
Q8. 客户端调用 EVALSHA 收到 NOSCRIPT 时,正确响应是?
- A.直接返回调用失败给业务方
- B.回退 EVAL 重发脚本再执行← 正确答案
- C.重启 Redis 让缓存重新加载
- D.改用 Pipeline 发送原始命令
答案:B。NOSCRIPT 表示脚本缓存被清理(重启或 SCRIPT FLUSH),编号查不到脚本。标准回退是重新用 EVAL 发送脚本,成功后 SHA 重新进入缓存,后续继续用 EVALSHA。A 会让业务无故失败;C 小题大做且重启后缓存仍空;D 丢掉了脚本的原子语义。
Q9. Lua 脚本死循环且已写命令,业务希望尽量保留数据,正确的是?
- A.用 SCRIPT KILL 强行终止并保留数据
- B.等待超时后脚本自动回滚继续服务
- C.SHUTDOWN NOSAVE 重启并承受缓存丢失← 正确答案
- D.重启后从 RDB 自动补齐丢失部分
答案:C。有写命令的脚本无法被 KILL,不会回滚,也无法保持“半写”一致性,所以唯一保底是 SHUTDOWN NOSAVE 放弃内存中未持久化的数据。A 违反 KILL 的适用条件;B 不存在自动回滚;D 中重启后只会加载磁盘已有快照,丢失部分不会被“补齐”。
Q10. 与 MULTI/EXEC 相比,Lua 脚本的事务性优势包括?
- A.脚本内能根据读取结果做判断← 正确答案
- B.原生原子性无需乐观锁重试← 正确答案
- C.出错时全部命令都会自动回滚
- D.一部脚本完成读改写复杂流程← 正确答案
答案:ABD。Lua 用 if 就能根据 GET 结果决定下一步,事务不行,A 对;脚本天然原子,不用 WATCH 重试,B 对;脚本不回滚,C 错;读改写一气呵成正是脚本的主场,D 对。