Redis执行Lua脚本通过单线程阻塞机制保证原子性:主线程独占执行整个脚本,期间拒绝其他请求,实现“读取-计算-写入”的不可见性。但原子性不提供回滚,已成功的命令无法撤销。集群环境下,脚本所有key必须显式声明在KEYS数组中以校验同一slot。
Redis执行Lua脚本能保证原子性,这个说法其实挺容易让人误解的。很多人第一反应是,它内部是不是有什么锁机制,或者像数据库那样有事务回滚?都不是。真相很简单,也很有Redis的风格:它就是在主线程里,以一种“独占式、阻塞式”的方式,把整个脚本一口气执行完。在这期间,任何其他客户端的请求都得排队等着,谁也别想插队。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
这背后的逻辑,想一下其实挺好理解的。Redis主线程是典型的事件循环模型,所有命令都得排着队等着被调度。当一个EVAL脚本进来之后:
redis.call()调用,走的是内部的执行队列,压根不需要再次解析和排队。GET、INCR或者SET请求。GET到一个值,正准备根据这个值往下算呢,不用担心别的客户端在中途把这个值给改了,这就是原子性的核心。SCRIPT KILL命令才行。聊到另一个常见问题:在Redis集群环境下,Lua脚本操作的所有key必须属于同一个slot。为什么官方文档反复强调要用KEYS[1]这种形式?因为Redis在脚本启动前,就能静态分析出脚本里涉及了哪些key,从而提前校验它们是否都在同一个slot上。但是,如果你在脚本里动态拼接key名,那就麻烦了。
redis.call("GET", KEYS[1]) —— 这能让Redis提前校验slot,没问题。local key = "user:" .. ARGV[1]; redis.call("GET", key) —— 这种动态生成的key,Redis没法提前知道它属于哪个slot,集群模式下直接就会报错 CROSSSLOT Keys in request don't hash to the same slot。ARGV只能用来传纯参数,千万别用它来构造key。如果脚本里需要操作多个key,那就必须把所有key都显式列在KEYS数组里。这是另一个非常常见的误解。Lua脚本的“原子性”,仅仅意味着执行过程不会被其他请求打断。但是,它不提供任何回滚能力。脚本内部已经成功执行的redis.call()命令,一旦写进去了,就永久生效了,不会有任何“撤销”操作。
redis.call("SET", "a", "1"),再执行 redis.call("INCR", "a")。如果第二行因为类型错误失败了(比如a的值不是数字),那第一行 SET 命令已经写进去的 “a” = “1” 还是会保留着,不会自动回滚到执行前的状态。pcall() 包裹关键操作,然后在捕获到错误时,手动调用 redis.call("DEL", ...) 来清理状态。redis.error_reply() 来抛错并中断脚本。但是注意,这只能阻止后续命令的执行,之前已经成功写入的命令,依然是不可逆的。业务上什么场景必须上Lua脚本?核心判断标准是:你的逻辑是不是一个需要“读取-判断-写入”的闭环,并且在这个闭环过程中,任何中间状态都不能被别的客户端看到或者修改。MULTI/EXEC 本质上只是把一些命令打包在一起执行,它并不能提供这种真正的原子性隔离。
SETNX的复杂条件操作。os.time() 这些非沙箱函数,不然会报 attempt to call a nil value 的错误。最后,很多人容易忽略一个点:所谓的“原子性”,仅仅覆盖了脚本里通过redis.call()调用的Redis命令。脚本内部自己的计算逻辑,比如字符串处理、循环,这些操作本身是不被保护的,它们一样会卡住主线程。只不过,它们不会产生“写坏数据”这种后果,更多的时候只是单纯地拖慢Redis响应而已。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述