Redis执行Lua脚本能保证原子性,根本原因是主线程独占式、阻塞式执行整个脚本,期间不处理任何其他客户端请求,所有redis.call()调用不可中断且无中间态。

Redis 执行 Lua 脚本能保证原子性,根本原因不是靠锁或事务回滚机制,而是它在主线程中**独占式、阻塞式执行整个脚本**——期间不处理任何其他客户端请求。
为什么单线程执行就等于原子性
Redis 主线程采用事件循环模型,所有命令(包括 EVAL)都排队等待调度。当一个 Lua 脚本开始执行时:
- 脚本被完整加载进 Lua 解释器,
redis.call()调用直接走内部执行队列,不重新解析、不重新排队 - 主线程不会切换上下文去处理其他客户端的
GET、INCR或SET请求 - 脚本里读、算、写三步操作对外“不可见中间态”——比如
GET到的值不会被其他客户端在脚本中途改掉 - 即使网络断开,脚本仍继续执行完;但若超时(默认 5 秒),Redis 只警告,不会自动终止(需手动
SCRIPT KILL)
KEYS 和 ARGV 的声明为什么影响集群可用性
Redis 集群要求所有操作的 key 必须落在同一 slot。脚本中用 KEYS[1] 是安全的,因为 Redis 启动前就能静态分析出涉及哪些 key;但若动态拼接 key 名,就会触发 CROSSSLOT Keys in request don't hash to the same slot 错误。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- ✅ 正确:
redis.call("GET", KEYS[1])—— Redis 可提前校验 slot - ❌ 错误:
local key = "user:" .. ARGV[1]; redis.call("GET", key)—— 集群模式下直接报错 - ARGV 只能传参数,不能用于构造 key;如需多 key,必须全部显式列在
KEYS数组里
脚本出错时到底会不会回滚
不会自动回滚。所谓“原子性”,仅指执行过程不被打断;但 Lua 脚本内已成功执行的 redis.call() 命令,一旦完成就永久生效。
- 例如:
redis.call("SET", "a", "1"); redis.call("INCR", "a")—— 若第二行因类型错误失败,"a"仍保持"1" - 想模拟回滚,得自己用
pcall()包裹关键操作,并在出错时手动redis.call("DEL", ...)或重置状态 -
redis.error_reply()可主动抛错中断脚本,但前面已成功的命令仍不可逆
什么时候该用 Lua 脚本而不是 MULTI/EXEC
当业务逻辑需要“读-判断-写”闭环,且不能容忍中间状态被其他客户端观测或修改时,必须选 Lua;而 MULTI/EXEC 只是命令打包,不提供真正原子性。
- 适合 Lua:秒杀扣库存(先查余量,再减)、分布式限流器(计数+过期时间同步设置)、带条件的 SETNX 替代方案
- 不适合 Lua:纯顺序写入多个 key(用 pipeline 更轻量)、需长时间遍历大量 key(会阻塞实例,应拆到客户端)
- 注意:Lua 脚本无法调用外部网络或文件,也不能用
os.time()等非沙箱函数,否则报attempt to call a nil value
真正容易被忽略的是:原子性只覆盖脚本内 redis.call() 调用,不覆盖 Lua 自身计算(比如字符串处理、循环);这些耗时操作一样会卡住主线程,只是不报错而已。

















