ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

一文搞懂 Redis 事务与 Lua 脚本:把它想成一家超市收银台

一文搞懂 Redis 事务与 Lua 脚本:把它想成一家超市收银台

文章目录

    • 一、先把超市和 Redis 对上号
    • 二、没有事务时,问题到底出在哪里
    • 三、MULTI 与 EXEC:先把商品全部扫码,再统一结账
      • 3.1 最基础的事务
      • 3.2 DISCARD:顾客说“这单不要了”
      • 3.3 为什么事务里拿不到上一条命令的结果
    • 四、Redis 事务不会自动回滚
      • 4.1 入队阶段就发现错误:整个事务拒绝执行
      • 4.2 执行阶段才出现错误:其他命令照常执行
    • 五、WATCH:结账前再看一眼商品价签
      • 5.1 WATCH 是乐观锁,不是把 Key 锁住
      • 5.2 正确的重试轮廓
      • 5.3 WATCH 的代价
    • 六、Pipeline 和事务不是一回事
    • 七、Lua:把结账规则直接交给 Redis 收银员
      • 7.1 一个完整的原子结账脚本
      • 7.2 Lua 为什么能解决竞态
      • 7.3 redis.call 与 redis.pcall
    • 八、KEYS 与 ARGV:商品编号别偷偷藏在操作手册里
    • 九、EVAL、SCRIPT LOAD 与 EVALSHA
    • 十、Lua 最大的风险:一位收银员把整家店堵住
    • 十一、事务、WATCH、Lua 和普通命令怎么选
      • 场景一:Redis 已经有一条原子命令
      • 场景二:多条命令只需要连续执行,不依赖中间结果
      • 场景三:需要先读取,在客户端计算,冲突概率较低
      • 场景四:需要读取、判断并修改多个 Key,追求一次原子完成
      • 场景五:只是想减少网络往返
    • 十二、完整 redis-cli 实验
    • 十三、放进真实系统前,还要补上六块拼图
      • 13.1 原子性不能代替幂等性
      • 13.2 客户端超时不等于服务端没有执行
      • 13.3 Lua 只保证 Redis 内部原子,管不到 MySQL 和消息队列
      • 13.4 脚本权限要跟着账号走
      • 13.5 故障转移后要准备重新加载脚本
      • 13.6 把冲突率和脚本耗时变成可观察指标
      • 13.7 事务与分布式锁不要互相冒充
    • 十四、十个高频误区
      • 误区 1:Redis 事务和 MySQL 事务一样
      • 误区 2:单线程就没有竞态条件
      • 误区 3:MULTI 后命令已经执行
      • 误区 4:事务中可以读取上一条结果再写下一条
      • 误区 5:WATCH 会锁住 Key
      • 误区 6:EXEC 失败应该无限重试
      • 误区 7:Lua 报错会撤销之前的写入
      • 误区 8:Lua 越大越能减少网络请求
      • 误区 9:EVALSHA 永远不会失败
      • 误区 10:Cluster 中 Lua 可以随便操作多个 Key
    • 十五、总结
    • 参考资料

周末晚上,超市收银台排起了长队。小林的购物篮里有牛奶、面包和一张满减券。收银员必须依次完成核对库存、扣减库存、使用优惠券、增加会员积分和生成小票。

要是处理到一半,另一名收银员突然插进来改库存;或者牛奶扣掉了,小票却没生成,这笔账就会变得很难看。

这正是很多 Redis 业务会遇到的问题:我们不是只想执行一条命令,而是希望一组操作按照约定的顺序完成,并且中间不要被其他客户端插队。

Redis 为此准备了两套常用工具:

  • MULTI / EXEC / WATCH,像把几项收银操作装进一个待执行清单;
  • Lua 脚本,像把完整结账规则交给一位熟练收银员,让判断和修改在 Redis 服务器里一次完成。

不过,Redis 事务和 MySQL 事务并不是同一种东西。Redis 没有传统意义上的自动回滚;Lua 虽然原子,却会阻塞服务器处理其他活动。工具用对了是收银台,用错了就可能变成堵住整个超市出口的购物车。

本文基于 Redis 8.6.1 实验,把它们的语义、差异和高频陷阱一次讲清。


一、先把超市和 Redis 对上号

超市里的角色Redis 概念负责什么
收银台Redis 服务端按顺序处理客户端命令
顾客Redis 客户端连接提交命令或事务
待结账清单MULTI后的命令队列暂存准备执行的命令
“开始结账”按钮EXEC连续执行队列中的命令
取消本单DISCARD清空队列并退出事务
盯住商品价签WATCH检查关键数据是否被别人改过
收银操作手册Lua 脚本在服务端完成判断与修改

先记住全文最重要的区别:

MULTI / EXEC擅长“把已有命令连续执行”;Lua 擅长“读取以后做判断,再决定执行哪些命令”。

如果只是同时给积分和订单数加一,事务清单就够用;如果要先检查库存是否充足,再扣库存并生成订单,Lua 往往更自然。


二、没有事务时,问题到底出在哪里

假设牛奶库存为 1,两名顾客几乎同时购买。应用采用最直白的三步:

GET stock:milk 判断库存大于 0 DECR stock:milk

可能出现这样的时序:

客户端 A:GET -> 1 客户端 B:GET -> 1 客户端 A:DECR -> 0 客户端 B:DECR -> -1

每一条 Redis 命令自身都是原子执行的,但“三条命令组成的业务逻辑”不是原子的。就像每次扫码都不会扫半件商品,却不代表两位收银员读取到的库存一定一致。

不少人会说:“Redis 不是单线程执行命令吗,怎么还会有并发问题?”

单线程保证的是:同一时刻不会把两条命令各执行一半。可客户端 A 的GETDECR之间,完全可能插入客户端 B 的命令:

A.GET -> B.GET -> A.DECR -> B.DECR

因此需要原子性的是业务动作,而不只是单条命令。


三、MULTI 与 EXEC:先把商品全部扫码,再统一结账

3.1 最基础的事务

MULTI SET checkout:receipt:1001 PAID INCR member:1001:points EXEC

交互结果类似:

OK QUEUED QUEUED 1) OK 2) (integer) 1

执行MULTI后,后续命令通常不会立即操作数据,而是返回QUEUED,表示已经放进当前连接的事务队列。直到EXEC到达,Redis 才依次执行它们,并按命令入队顺序返回结果数组。

这套机制有两个重要保证:

  1. EXEC执行队列期间,其他客户端的命令不会插入事务中间;
  2. 如果客户端在发送EXEC以前断开,队列里的命令不会执行。

注意,事务属于当前连接。不能在连接 A 上MULTI,然后换连接 B 去EXEC。使用连接池时,必须确保整个过程绑定同一条连接。

3.2 DISCARD:顾客说“这单不要了”

MULTI SET checkout:receipt:1002 PAID INCR member:1002:points DISCARD

DISCARD会清空已入队的命令并退出事务,两个写操作都不会发生。它不是“执行失败后的回滚”,而是在 EXEC 之前主动放弃队列

3.3 为什么事务里拿不到上一条命令的结果

下面这个想法看起来合理:

MULTI GET stock:milk 根据 GET 结果决定是否 DECR EXEC

问题在于GET只会返回QUEUED,真实库存要等EXEC时才读取。应用在组装队列时拿不到这个结果,自然无法根据它决定下一条命令。

MULTI / EXEC不是存储过程,也不是一段带if的程序。需要“先读、判断、再写”时,可以用WATCH做乐观锁,或者直接用 Lua 把判断搬到服务端。


四、Redis 事务不会自动回滚

这是 Redis 事务和数据库事务最容易混淆的地方。

4.1 入队阶段就发现错误:整个事务拒绝执行

比如命令参数数量不对:

MULTI SET only-key INCR checkout:count EXEC

SET only-key在排队时就能发现语法或参数错误。此时事务会被标记为无效,EXEC返回EXECABORT,队列中的命令都不执行。

这像收银员在扫码阶段就发现“这张操作单连商品编号都没写”,于是整单不结。

4.2 执行阶段才出现错误:其他命令照常执行

SET checkout:points hello MULTI SET checkout:receipt PAID INCR checkout:points SET checkout:noticedoneEXEC

INCR只有真正执行时才知道 Value 不是整数。它会返回错误,但前后的SET仍然执行。Redis 不会把已经成功的命令撤销。

就像小票打印到第二行时发现会员积分格式坏了:Redis 会报告这一项失败,却不会倒带撕掉前面已经打印的内容。

Redis 官方明确说明事务不支持回滚。这样做让实现更简单、执行更快,也要求开发者:

  • 提前校验输入;
  • 让事务中的命令类型和 Key 结构保持可预测;
  • 不要把“部分命令可能运行时失败”的流程当成数据库 ACID 事务。

五、WATCH:结账前再看一眼商品价签

5.1 WATCH 是乐观锁,不是把 Key 锁住

库存需要先读取再计算时,可以这样做:

WATCH stock:milk GET stock:milk # 客户端计算新库存 MULTI SET stock:milk 8 EXEC

WATCHEXEC之间,如果被监视的 Key 发生修改,EXEC会返回空结果,整组事务不执行。应用需要重新读取、重新计算并重试。

它像收银员先记下价签是 10 元,准备结账时发现另一位员工把价签换成 12 元,于是放弃本次结算,重新核价。

WATCH并不会阻止别人修改 Key,所以它是乐观锁:先假设冲突不常发生,真冲突了再重试。

5.2 正确的重试轮廓

for 有限次数: WATCH key 读取数据并校验 如果业务条件不成立: UNWATCH 返回业务失败 MULTI 写入新值 result = EXEC 如果 result 成功: 返回成功 随机退避后重试

需要注意:

  • EXEC成功、失败或连接断开后,监视状态都会结束;
  • 不继续事务时应执行UNWATCH,让连接尽快回归普通状态;
  • 重试必须有上限和退避,热点 Key 冲突严重时无限自旋只会加重拥堵;
  • Key 的过期和淘汰也可能被视为修改,从而让事务放弃;
  • Redis 6.0.9 之前,过期 Key 对WATCH的行为有历史差异。

Redis 8.4 起,字符串SET增加了IFEQIFNE等比较后写入选项,简单的字符串 CAS 可以用一条命令完成。但涉及多个数据结构或复杂判断时,WATCH和 Lua 仍然有价值。使用新选项前要确认生产版本和客户端支持情况。

5.3 WATCH 的代价

低冲突时,乐观锁很轻巧;高冲突时,大量客户端会反复经历“读取—计算—EXEC 失败—重试”。如果所有人都在抢最后一盒牛奶,Lua 通常比让一百名收银员反复核价更合适。


六、Pipeline 和事务不是一回事

Pipeline 经常和事务一起被提到,因为它们都能一次发送多条命令,但目标完全不同。

对比项PipelineMULTI / EXEC
核心目标减少网络往返保证一组命令连续执行
是否阻止其他客户端插入不保证EXEC阶段保证
是否返回每条命令结果
是否能提高吞吐通常可以不一定
是否等于业务原子性只覆盖队列执行阶段

Pipeline 像顾客一次把十件商品放上传送带,减少来回递商品的次数;事务像按下“本单连续处理”按钮。很多客户端支持“事务 + Pipeline”,但要清楚自己要解决的是网络往返,还是并发一致性。


七、Lua:把结账规则直接交给 Redis 收银员

7.1 一个完整的原子结账脚本

下面的脚本先检查库存,再扣减库存、生成订单并设置过期时间:

localstock_key=KEYS[1]localorder_key=KEYS[2]localsku=ARGV[1]localquantity=tonumber(ARGV[2])localamount=ARGV[3]ifnotquantityorquantity<=0thenreturnredis.error_reply("INVALID_QUANTITY")endlocalstock=tonumber(redis.call("HGET",stock_key,sku)or"-1")ifstock<0thenreturnredis.error_reply("SKU_NOT_FOUND")endifstock<quantitythenreturn{0,stock}endlocalremaining=redis.call("HINCRBY",stock_key,sku,-quantity)redis.call("HSET",order_key,"sku",sku,"quantity",quantity,"amount",amount,"status","PAID")redis.call("EXPIRE",order_key,3600)return{1,remaining}

执行:

redis-cli--evalcheckout.lua\"{store}:stock""{store}:order:1001",\milk218.80

逗号左边是KEYS,右边是ARGV。脚本返回:

1) (integer) 1 2) (integer) 3

第一个数字表示购买成功,第二个是剩余库存。如果库存不足,脚本在任何写入发生前返回{0, 当前库存},订单不会创建,库存也不会扣减。

7.2 Lua 为什么能解决竞态

Redis 保证脚本原子执行。脚本运行期间,其他客户端不会在脚本调用的多条 Redis 命令之间插入操作。

以前的流程是:

客户端读取 -> 网络返回 -> 客户端判断 -> 网络发送 -> Redis 修改

Lua 以后变成:

客户端提交脚本 -> Redis 内部读取、判断、修改 -> 返回结果

判断靠近数据,既减少网络往返,又让检查和写入形成一个不可插队的整体。

但“原子”不等于“自动回滚”。如果脚本先完成写操作,后来某条redis.call报错,前面的写入不会自动撤销。因此脚本也应该先完成参数、类型和业务条件校验,再进入写入阶段。

7.3 redis.call 与 redis.pcall

  • redis.call(...)遇到错误会终止脚本,并把错误返回客户端;
  • redis.pcall(...)把错误作为 Lua 值返回,脚本可以自行判断和处理。
localresult=redis.pcall("INCR",KEYS[1])ifresult.errthenreturn{0,result.err}endreturn{1,result}

不要为了“脚本不报错”而到处使用pcall。真正无法恢复的类型错误,直接失败通常更容易暴露数据污染。


八、KEYS 与 ARGV:商品编号别偷偷藏在操作手册里

EVAL的格式是:

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

所有 Redis Key 都应该通过KEYS显式传入,普通参数放进ARGV

redis.call("HINCRBY",KEYS[1],ARGV[1],-tonumber(ARGV[2]))

不要在脚本里动态拼出一个未声明的 Key:

-- 不推荐localkey="order:"..ARGV[1]redis.call("HSET",key,"status","PAID")

显式声明 Key 有三个好处:

  1. Redis 和客户端更容易分析脚本会访问哪些数据;
  2. 同一份脚本可以通过参数复用,避免生成大量不同脚本;
  3. Redis Cluster 能检查这些 Key 是否位于兼容的 Hash Slot。

Cluster 中,多 Key 脚本通常要求 Key 在同一个槽。可以使用 Hash Tag:

{store}:stock {store}:order:1001

花括号中的store相同,两者会映射到同一个槽。它不是为了让 Key 更好看,而是数据分片设计的一部分。把所有业务 Key 都塞进同一个 Hash Tag 又会制造热点,所以应按业务聚合边界设计,而不是“一把梭”。


九、EVAL、SCRIPT LOAD 与 EVALSHA

每次EVAL都发送完整脚本,简单但会增加网络传输。生产中常用:

SCRIPT LOAD"return redis.call('GET', KEYS[1])"

Redis 返回脚本内容的 SHA1 摘要:

4e6d8fc8bb01276962cce5371fa795a7763657ae

之后使用:

EVALSHA 4e6d8fc8bb01276962cce5371fa795a7763657ae1stock:milk

脚本缓存不是数据库数据,不会可靠持久化。重启、故障转移或SCRIPT FLUSH后,EVALSHA可能返回:

NOSCRIPT No matching script. Please use EVAL.

客户端的正确策略是:优先EVALSHA;遇到NOSCRIPT时加载脚本,再重试。许多 Redis 客户端已经封装了这个过程。

千万不要把商品 ID、用户 ID 直接拼进脚本文本,每次生成一份不同脚本。Redis 会按 SHA1 缓存它们,动态脚本可能让脚本缓存不断增长。正确做法是脚本固定,变化值走KEYSARGV

Redis 7 起还提供 Redis Functions:代码以函数库形式加载、命名并由服务器管理,适合希望把可复用逻辑作为 Redis 部署的一部分。EVAL脚本更像客户端携带的临时操作手册;Functions 更像正式登记在超市后台的标准流程。本文以基础场景常见的 Lua Eval 为主。


十、Lua 最大的风险:一位收银员把整家店堵住

Redis 脚本原子执行的另一面,是脚本运行期间会阻塞服务器处理其他活动。下面这些操作非常危险:

  • 在 Lua 中遍历海量 Key 或巨大集合;
  • 写一个次数不可控的循环;
  • 把复杂报表统计塞进脚本;
  • 一次操作大量大 Key;
  • 在线上临时执行未经压测的脚本。

脚本应该“小、快、边界明确”。复杂度取决于脚本内部调用的命令,EVAL不会把 O(N) 魔法变成 O(1)。

排查时可关注:

SLOWLOG GET10INFO commandstats INFO memory

还要监控 Redis 延迟、CPU、阻塞客户端数和脚本执行错误。

SCRIPT KILL也不是万能急停按钮:对于尚未执行写命令的脚本,Redis 可以终止;如果脚本已经写入数据,为避免留下不可理解的半状态,处理方式会更受限制。真正的安全策略是上线前限定输入规模、做基准测试,并设置合理的客户端超时和 Redis Lua 时间限制。


十一、事务、WATCH、Lua 和普通命令怎么选

场景一:Redis 已经有一条原子命令

直接用命令:

INCR counter HINCRBY cart:1001 sku:milk1SET lock:value token NX PX30000

不要为了“显得高级”套事务或 Lua。

场景二:多条命令只需要连续执行,不依赖中间结果

使用MULTI / EXEC。例如同时写订单状态、增加积分和记录统计,而且这些命令的类型错误可以提前避免。

场景三:需要先读取,在客户端计算,冲突概率较低

使用WATCH + MULTI / EXEC。要有有限重试和退避,并记录冲突率。

场景四:需要读取、判断并修改多个 Key,追求一次原子完成

使用 Lua。脚本必须短小,Key 边界明确,并考虑 Cluster 同槽。

场景五:只是想减少网络往返

使用 Pipeline。别把吞吐优化误认为事务语义。


十二、完整 redis-cli 实验

实验使用独立端口6395,关闭 RDB 和 AOF,不连接本机常用的6379

chmod+x run-lab.sh ./run-lab.sh

脚本会验证:

  • MULTI / EXEC的入队与执行结果;
  • DISCARD后 Key 不存在;
  • 两个客户端竞争时WATCH事务放弃;
  • Lua 结账成功后库存正确扣减;
  • 库存不足时订单不创建、库存不变化;
  • SCRIPT LOAD / EVALSHA正常执行;
  • SCRIPT FLUSH后出现NOSCRIPT

实验中的库存和订单 Key 统一使用{store}Hash Tag;由于临时实例是非 Cluster 模式,脚本不执行槽位命令,Cluster 部署时应在目标集群用CLUSTER KEYSLOT再次核对。

关键输出类似:

== WATCH abort with two clients == OK 10 OK QUEUED watch_result=aborted, stock=9 == Lua atomic checkout == 1 3 0 3 stock_after_success_and_failure=3 == SCRIPT LOAD / EVALSHA / NOSCRIPT == sha=... 1 NOSCRIPT No matching script. Please use EVAL. ALL ASSERTIONS PASSED

临时实例结束后会执行SHUTDOWN NOSAVE。如果6395已被其他进程占用,实验会直接退出,绝不会停止未知进程。


十三、放进真实系统前,还要补上六块拼图

13.1 原子性不能代替幂等性

假设 Lua 已经成功扣库存并创建订单,但网络在返回结果前断开。客户端不知道这单到底成功没有,如果直接原样重试,可能再次扣库存。

因此脚本应该携带业务幂等号,例如订单 ID,并在开头检查订单是否已经存在:

ifredis.call("EXISTS",KEYS[2])==1thenreturn{2,redis.call("HGET",KEYS[2],"status")}end

这里的2可以表示“此前已经处理过”。客户端收到超时后,使用同一个订单 ID 重试,脚本会返回旧结果,而不是重复结账。

要注意,幂等记录的保存时间必须覆盖业务可能重试的时间窗口。如果订单 Key 五秒就过期,十秒后到达的重试还是会被当成新订单。

13.2 客户端超时不等于服务端没有执行

客户端等待 100 毫秒后超时,只能说明“没有按时收到响应”,不能说明 Redis 没处理命令。网络抖动、连接复用、慢脚本和客户端 GC 都可能造成超时。

所以涉及资金、库存、券核销时,不要写出这样的逻辑:

请求超时 -> 当作失败 -> 换一个订单号重试

更安全的做法是使用固定幂等号查询最终状态,区分“明确失败”和“结果未知”。原子性保护的是 Redis 内部操作边界,幂等性保护的是客户端重试边界,两者缺一不可。

13.3 Lua 只保证 Redis 内部原子,管不到 MySQL 和消息队列

如果结账流程还包含:

Redis 扣库存 MySQL 写订单 Kafka 发消息 第三方支付

一段 Redis Lua 不可能让四套系统一起提交或回滚。脚本成功后 MySQL 仍可能失败,支付成功后消息也可能暂时发送不出去。

这类跨系统一致性需要状态机、事务消息、Outbox、补偿任务或对账机制。不要因为 Lua 脚本里有“原子”两个字,就把它升级成分布式事务。

超市里的收银员只能保证本台收银机的动作连续,管不了银行清算中心和仓库运输车同时成功。

13.4 脚本权限要跟着账号走

生产环境通常通过 ACL 限制应用账号。使用 Lua 时,不仅要允许EVALEVALSHA,还要确保脚本内部调用的 Redis 命令也在账号权限范围内。

权限不应该大到“为了脚本方便直接给+@all”。更稳妥的是根据脚本真实使用的命令和 Key 前缀配置最小权限,并在预发布环境用与生产一致的账号验证。

脚本也不能访问 Redis 主机的文件系统、网络或任意系统调用。Lua 运行在受限沙箱里,它是数据旁边的小程序,不是让你远程登录服务器的后门。

13.5 故障转移后要准备重新加载脚本

EVAL脚本缓存是易失的。主节点重启,或者 Sentinel/Cluster 发生主从切换后,新主节点未必拥有客户端记住的脚本 SHA1。

因此部署过程最好做到:

  1. 应用启动时加载核心脚本,但不要假设预加载永远有效;
  2. 正常调用优先使用EVALSHA
  3. 捕获NOSCRIPT后,在当前目标节点重新SCRIPT LOAD
  4. 加载完成后只重试一次,并继续使用原幂等号;
  5. NOSCRIPT数量和故障转移事件建立监控。

如果采用 Redis Functions,还要把函数库部署纳入 Redis 实例的版本与变更管理,不能只更新应用代码却忘了服务端函数。

13.6 把冲突率和脚本耗时变成可观察指标

仅看 Redis QPS,很难判断事务是不是健康。建议至少记录:

  • WATCHEXEC放弃次数和重试次数;
  • Lua 成功、库存不足、参数错误、NOSCRIPT的数量;
  • 脚本端到端耗时及 P95、P99;
  • RedisSLOWLOG、命令调用次数、CPU 和延迟;
  • 热点 Key、单次脚本涉及的元素数量;
  • 客户端超时后通过幂等查询确认成功的比例。

如果WATCH冲突率持续升高,说明“乐观地认为大家不会撞车”已经不成立,应考虑单条原子命令、Lua、分片或业务队列。如果 Lua P99 明显上升,则要检查脚本复杂度、大 Key 和输入规模,而不是简单把客户端超时调大。

13.7 事务与分布式锁不要互相冒充

WATCH不是分布式锁,MULTI / EXEC也不会在业务处理期间替你占住资源。反过来,拿到分布式锁也不代表锁内每个 Redis 命令会自动回滚。

如果业务真的需要“某段跨 Redis、数据库和外部调用的流程同一时间只允许一个执行者”,才考虑分布式锁,并处理唯一令牌、续期、安全解锁和故障恢复。仅仅为了防止库存检查与扣减被插队,短小的 Lua 往往比“加锁—网络往返—解锁”更直接。

关于锁的完整边界,可以继续看《一文搞懂 Redis 分布式锁》。


十四、十个高频误区

误区 1:Redis 事务和 MySQL 事务一样

Redis 保证队列连续执行,但不提供传统事务的自动回滚语义。

误区 2:单线程就没有竞态条件

单条命令不会被拆开,不代表多条命令之间不会插入别人的操作。

误区 3:MULTI 后命令已经执行

绝大多数命令只是返回QUEUED,真正执行发生在EXEC

误区 4:事务中可以读取上一条结果再写下一条

组装队列时拿不到真实结果。需要WATCH或 Lua。

误区 5:WATCH 会锁住 Key

它只观察变化,不阻止别人写;冲突时由当前事务放弃并重试。

误区 6:EXEC 失败应该无限重试

热点冲突下无限重试会制造流量风暴。必须限制次数并退避。

误区 7:Lua 报错会撤销之前的写入

不会。应先验证,再写入,减少脚本中途出错的机会。

误区 8:Lua 越大越能减少网络请求

大脚本会长时间阻塞 Redis。脚本应只承载靠近数据的短逻辑。

误区 9:EVALSHA 永远不会失败

脚本缓存可能丢失,客户端必须能处理NOSCRIPT

误区 10:Cluster 中 Lua 可以随便操作多个 Key

多 Key 脚本要考虑 Hash Slot。Hash Tag 能解决同槽问题,也可能带来热点。


十五、总结

把 Redis 想成一家超市后,这几种工具就很好区分:

  • 普通原子命令,是收银员一次完成一个标准动作;
  • Pipeline,是把一篮子商品一次送上输送带,减少来回跑;
  • MULTI / EXEC,是先列清单,再连续执行本单操作;
  • DISCARD,是在结账前取消整张清单;
  • WATCH,是结账前检查价签是否被别人换过,冲突就重来;
  • Lua,是把读取、判断和修改写成一份服务端操作手册;
  • 原子执行不等于自动回滚,脚本也不例外;
  • Lua 越长,其他顾客等待越久;
  • Cluster 中还要考虑 Key 是否同槽。

真正成熟的做法,不是所有业务都套 Lua,而是先寻找 Redis 已有的原子命令;解决不了,再根据是否依赖读取结果、冲突概率、脚本复杂度和集群结构选择事务、WATCH 或 Lua。

一句话收尾:

事务负责“不让别人插队”,WATCH 负责“发现价签变了”,Lua 负责“把核价和结账一次做完”。


参考资料

  • Redis Transactions 官方文档
  • Scripting with Lua 官方文档
  • EVAL 命令文档
  • Redis Lua API Reference
  • Redis Functions 官方文档

本文实验基于 Redis 8.6.1。Redis 事务和 Lua 的核心语义较稳定,但命令选项、Functions 与集群能力会随版本演进,生产使用前请核对目标版本官方文档。


  • 个人小游戏

返回列表