redis multi/exec 仅保证命令执行顺序原子性,不检查语义冲突、不阻塞并发读写、不回滚错误命令;真原子需用lua脚本,因其单线程全执行、异常即终止、支持跨类型联动与keys/argv安全隔离。

Redis MULTI/EXEC 无法跨数据类型保证原子性
因为 MULTI/EXEC 只是命令队列化,不检查语义冲突,更不阻塞其他客户端的读写。比如你用 SET 写字符串、再用 HSET 写哈希、再用 SADD 写集合——这些命令在 EXEC 中确实按序执行,但中间若被其他客户端修改了某个 key(比如另一个 client 在你 EXEC 前删掉了那个 hash),整个流程就失效了,而 Redis 不会回滚已执行的部分。
常见错误现象:EXEC 返回部分成功结果(如 (integer) 1, (nil), (integer) 0 混合),但业务逻辑已经断裂;或脚本外的并发写入导致条件判断失效(如“先 GET 再 SET”被插入)。
-
MULTI/EXEC是“伪原子”:只保证命令入队和出队顺序,不保证执行时数据状态一致 - 不同数据类型(string/hash/list/set/zset)的操作无法共享上下文,
WATCH对多 key + 多类型变更的监听不可靠 - 一旦某条命令报错(如对 string 执行
HGET),后续命令仍会继续执行,不会中断或回滚
Lua 脚本中 redis.call() 的执行是真原子
Redis 单线程模型下,整个 Lua 脚本从解析到结束都在一个事件循环内完成,期间不会有其他客户端命令插入。这意味着你可以安全地混合使用 GET、HGETALL、ZRANK、LPUSH 等任意类型命令,并基于返回值做分支判断。
例如实现“用户积分更新+排行榜同步+过期清理”三步联动:先查当前积分,若达标则更新 hash 字段、插入 zset、并删除旧缓存——这三步要么全成,要么全不执行,且中间状态对外不可见。
-
redis.call()抛出异常会直接终止脚本,整个执行过程回滚(无中间态残留) - 所有 key 必须显式声明在
KEYS数组里,Redis 可据此做 key 预检和 cluster slot 路由 - 支持嵌套逻辑:比如遍历
SCAN结果后批量调用DEL,或根据zscore动态决定是否触发通知
KEYS 和 ARGV 的分离设计强制解耦与安全
Redis 要求所有操作的 key 名必须通过 KEYS 数组传入,不能硬编码或拼接。这不是限制,而是为集群模式和脚本校验打基础——只有明确声明的 key,Redis 才能确保它们落在同一 slot,避免 CROSSSLOT 错误。
而值、阈值、超时时间等动态参数必须走 ARGV,这样脚本能被反复复用(比如同一段限流逻辑,只需换 ARGV[1] 的 QPS 限值)。
- 违反规则会直接报错:
ERR Lua script attempted to access a non local key - 硬编码 key 在 Redis Cluster 下必然失败,而
KEYS[1]可被自动路由 - 客户端必须严格匹配
numkeys参数,少传或多传都会触发ERR Error running script
evalsha 缓存机制让复杂脚本真正可工程化
每次用 EVAL 发送几百行 Lua 是反模式。真实项目中应先用 SCRIPT LOAD 注册脚本,拿到 SHA1 后用 EVALSHA 调用。Redis 服务端会缓存已加载脚本,后续调用不传源码、不重复解析,性能接近原生命令。
这使得多数据类型事务脚本能像函数一样被版本管理、灰度发布、监控追踪——比如把“订单创建+库存扣减+优惠券核销”封装为一个 SHA,不同微服务共用同一份逻辑。
- 未缓存脚本用
EVAL,首次执行慢且占带宽;已缓存脚本用EVALSHA,耗时通常 - 脚本超时会被
SCRIPT KILL强制终止,但前提是没执行写操作;否则只能SHUTDOWN或等自然结束 - 建议单个脚本执行时间控制在 10ms 内,避免阻塞其他请求;复杂计算尽量前置到客户端
MULTI/EXEC 的能力边界就到了。Lua 提供的不是语法糖,是唯一能在 Redis 单线程模型下守住数据一致性的执行容器。稍不注意,就会在集群环境或高并发下漏掉 key 路由校验、脚本超时、或 ARGV 类型转换错误(比如把字符串当数字用 tonumber() 失败)。











