因为get和set是两个独立命令,中间存在时间窗口,客户端a读取旧值后、写入前,客户端b可能已修改该key,导致a覆盖b的结果;而redis单线程仅保证单命令原子性,不保证多命令组合的原子性。

为什么直接用 GET + SET 无法保证原子性?
因为网络往返和 Redis 命令执行之间存在时间窗口:客户端 A 读到旧值,还没来得及写入,客户端 B 已经修改了 key,A 的写入就覆盖了 B 的结果。这不是 Redis 的问题,而是应用层并发控制缺失。
Redis 本身不提供类似数据库的“乐观锁字段”机制,但它的 Lua 脚本在服务端以原子方式执行——整个脚本运行期间不会被其他命令打断,这是实现 CAS 的基础。
- 必须用
EVAL或EVALSHA提交脚本,不能拆成多条命令 - 脚本内不能依赖外部状态(比如系统时间、随机数),否则不可重现
- 所有 key 必须通过
KEYS数组传入,否则集群模式下会报CROSSSLOT错误
EVAL 中如何安全比对并更新?
核心逻辑是:读取当前值 → 比对是否等于预期旧值 → 相等则写入新值,返回成功;否则返回失败。Lua 脚本里用 redis.call("GET", KEYS[1]) 读,用 redis.call("SET", KEYS[1], ARGV[2]) 写,全程无竞态。
示例脚本(检查 key 是否等于 ARGV[1],是则设为 ARGV[2]):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[2])
return 1
else
return 0
end
-
KEYS[1]是要操作的 key 名(如"user:100:balance") -
ARGV[1]是期望的旧值,ARGV[2]是目标新值 - 返回
1表示更新成功,0表示被其他请求抢先修改 - 注意:如果 key 不存在,
GET返回nil,而nil == "xxx"在 Lua 中恒为false,行为符合预期
遇到 ERR Error running script 或 NOAUTH 怎么办?
常见报错不是逻辑错,而是权限或语法边界问题:
-
ERR Error running script (OOM):脚本太长或嵌套过深,Redis 默认限制 512MB 内存和 100 层嵌套,精简逻辑,避免循环中调用redis.call -
NOAUTH Authentication required:连接未认证就执行EVAL,先AUTH再运行,或改用带认证的连接池 -
ERR wrong number of arguments:传入的KEYS或ARGV数量不对,比如脚本用了KEYS[1]却只传了空数组 - 集群环境下忘记把所有涉及的 key 放进
KEYS数组,导致CROSSSLOT报错,哪怕只是读一个、写另一个,也必须同 slot
性能和可维护性要注意什么?
Lua 脚本虽原子,但不是银弹。高频 CAS 场景下,反复失败重试会放大延迟,且脚本一旦上线就难热更新。
- 不要在脚本里做复杂计算(如 JSON 解析、正则匹配),Redis 的 Lua 环境没这些库,且会阻塞主线程
- 生产环境建议用
EVALSHA替代EVAL:先SCRIPT LOAD缓存脚本,后续用 sha1 值调用,减少网络传输和解析开销 - 如果 CAS 失败率持续 > 10%,说明竞争激烈,该考虑分片(如按用户 ID 取模分 key)或改用
INCRBY/HINCRBY等原生命令 - 调试时用
redis-cli --eval测试脚本,注意参数顺序:--eval script.lua key1 key2 -- arg1 arg2,中间--不能省
真正难的不是写对脚本,而是判断什么时候不该用 CAS——比如计数器、库存扣减这类场景,优先查有没有更轻量的原生命令能覆盖。










