Redis Lua脚本无回滚机制,执行失败后已写数据不撤销;原子性仅指单线程不中断,非ACID原子性;需前置校验+pcall捕获+写操作置尾来保障安全。

Redis Lua脚本执行失败后修改的数据不回滚,不是缺陷,是设计选择——它压根没实现回滚能力,也没有 undo log、事务日志或两阶段提交机制。
redis.call() 报错就停,已写的 key 不会撤销
脚本里调用 redis.call("SET", "a", "1") 成功后,紧接着 redis.call("INCR", "b") 因 b 是字符串而报错:(error) ERR value is not an integer,此时 a 的值仍是 "1",不会恢复成旧值或被删除。
这是因为 Redis 的“原子性”仅指单线程内执行不被中断,不是 ACID 里的原子性。官方文档白纸黑字写着:“Lua scripts are executed atomically, but there is no rollback.”
-
redis.call()是硬执行:出错立即终止脚本,但不擦除前面已生效的写操作 - 所有写命令(
SET、HSET、LPUSH等)一旦成功返回,就永久落库 - 没有后台线程去扫描“哪些命令执行了一半”,更不会自动逆向执行
DEL或SET回旧值
想“类回滚”?得靠 pcall + 显式判断 + 前置校验
不能指望 Redis 自动兜底,只能在脚本里手动控制风险边界。核心思路是:把破坏性操作放在所有检查之后,确保“全绿灯才动数据”。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用
redis.pcall()替代redis.call()捕获错误,避免脚本直接崩掉,从而有机会做清理或跳过 - 读操作必须放最前,并显式检查返回值:
if not bal then return redis.error_reply("missing") - 关键业务条件(如余额是否充足、key 是否存在、type 是否匹配)全部用
redis.call("EXISTS")或redis.call("TYPE")验证,任一失败立刻return - 写操作统一堆在最后,中间不夹杂任何可能失败的
redis.call
示例片段:
local bal = redis.call("GET", KEYS[1])
if not bal or tonumber(bal) < tonumber(ARGV[1]) then
return redis.error_reply("insufficient")
end
redis.call("DECRBY", KEYS[1], ARGV[1])
redis.call("LPUSH", KEYS[2], ARGV[2])
return "ok"
集群环境下还多一层限制:KEYS 必须同 slot,否则脚本根本跑不起来
在 Redis Cluster 中,Lua 脚本连“执行不中断”这个基本原子性都保不住——如果 KEYS 分散在不同 slot,Redis 直接拒绝执行,报错 (error) CROSSSLOT Keys in request don't hash to the same slot。
-
KEYS参数必须是调用时传入的固定字符串数组,不能拼接:"user:"..ARGV[1]是非法的 - 要用 hash tag 对齐 slot,比如
{user:123}:balance和{user:123}:log才能进同一个 slot - 客户端传参时若漏传某个
KEYS[i],脚本运行时报ERR Error running script (call to f_): @user_script:3: user_script:3: attempt to index a nil value
真正容易被忽略的点是:脚本里哪怕只写了一行 redis.call("SET", ...),只要它执行成功,这个副作用就不可逆。你无法靠“加个 pcall 包一下”来让已发生的写入消失——pcall 只影响后续逻辑流,不影响已经落盘的状态。安全边界只能划在写之前,而不是写之后补救。

















