Redis Lua脚本不满足ACID中的原子性回滚,因无UNDO LOG,已执行写入不可撤销;仅保证执行过程不中断,失败后状态可能不一致。

Redis Lua 脚本无法实现真正意义上的“多 Key 事务回滚”——它只保证执行过程不被中断,不保证失败后已写数据能撤回。
为什么 EVAL 脚本不是 ACID 事务
很多人看到 EVAL 把多条命令包在一起,就默认它等同于 MySQL 的 BEGIN/COMMIT/ROLLBACK。但事实是:Redis 没有 UNDO LOG,也没有两阶段提交。脚本中一旦 redis.call("SET", KEYS[1], "done") 成功,这个写入就永久生效;后续若 redis.call("INCR", KEYS[2]) 报错(比如 key 不存在或类型不对),前面的 SET 不会自动撤销。
常见错误现象:
- 脚本返回
(error) ERR Error running script,但部分 key 已被修改,状态不一致 - 用
redis.pcall()捕获错误后尝试手动DECR补偿,结果补偿操作又失败,雪上加霜 - 并发场景下两个脚本都读到旧值、都通过条件判断、都写入,最终覆盖对方结果
怎样写出“安全”的多 Key 原子脚本
所谓“安全”,是指把失败控制在写入之前,靠条件判断 + 提前退出,而不是事后补救。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 所有读操作必须放在写操作之前,并用
tonumber()、type()或== nil显式校验返回值 - 用
if判断业务条件(如余额是否充足、状态是否允许),不满足就直接return redis.error_reply("xxx") - 写操作全部放在所有判断之后,确保“全成功才写,任一失败不写任何东西”
- 集群模式下,所有
KEYS必须落在同一 slot,推荐用{user:123}这种 hash tag 对齐
示例:扣款前检查余额和状态,任一不满足就不动任何 key
EVAL "local bal = redis.call('GET', KEYS[1]); if not bal then return redis.error_reply('balance missing') end; if 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'" 2 balance:123 log:123 100 "order_abc"
KEYS 和 ARGV 传参必须严格区分
这是集群环境下最常踩的坑:脚本里不能动态拼接 key 名。
-
KEYS只能是调用时显式传入的字符串数组,Redis 靠它做 slot 路由和 key 预检 -
ARGV是任意字符串,适合传数值、flag、JSON 片段等非 key 数据 - 错误写法:
local k = "user:" .. ARGV[1]; redis.call('GET', k)→ 触发CROSSSLOT Keys in request don't hash to the same slot - 正确写法:调用时传
user:123作为KEYS[1],脚本里直接用KEYS[1]
超时与阻塞风险比想象中更严重
脚本在 Redis 主线程中同步执行,一旦卡住,整个实例请求都会排队。
- 默认超时 5 秒,超时后连接断开,但已执行的
redis.call()不会回退 - 禁止在脚本里做循环遍历大量 key、JSON 解析、正则匹配、sleep 等耗时操作
- 调试时别依赖
print()(Redis Lua 环境不支持),改用redis.log()写日志 - 生产环境优先用
SCRIPT LOAD+EVALSHA,避免每次传输大脚本体
真正需要跨 key / 跨系统一致性时,Lua 不是终点,而是起点——得配合客户端幂等、TCC、外部事务日志这些机制一起兜底。脚本越短、越薄、越只做“读-判-写”三件事,就越可靠。

















