redis单命令虽原子,但“读-判-写”两步操作存在竞态,必须用lua脚本将三步锁死在服务端原子执行;脚本需通过keys传参、避免不可重现函数、集群下确保所有key同槽,并推荐script load+evalsha优化性能。

为什么不能直接用 SET 或 INCRBY 做条件更新
Redis 的单命令确实是原子的,但“先读再判断再写”这种两步操作天然不是原子的——哪怕只差几微秒,也可能被其他客户端插进来改掉值。比如想实现“只有当前值小于 100 才加 10”,用 GET + SET 组合必然有竞态。Redis 不提供内置的带条件的写命令(像 SQL 的 UPDATE ... WHERE),所以必须靠 Lua 脚本把读、判、写三步锁死在服务端一次执行。
怎样写一个安全的条件更新 Lua 脚本
Lua 脚本在 Redis 中以原子方式执行,且能访问所有 key(只要在 EVAL 命令中声明)。关键点是:脚本里用 redis.call() 读写,所有逻辑走服务端,不暴露中间状态。
例如实现“若 key 存在且值为数字、且小于 100,则 +10,否则不操作并返回原值”:
if redis.call('EXISTS', KEYS[1]) == 1 then
local val = tonumber(redis.call('GET', KEYS[1]))
if type(val) == 'number' and val <p>调用时注意:</p>
-
EVAL第二个参数是 key 个数,这里只操作一个 key,填1 - key 名必须放在
KEYS数组里,变量值放ARGV,不能硬编码在脚本中(否则无法复用) - 脚本里不能用
math.random()等不可重现函数,否则主从复制可能出错 - 如果涉及多个 key,必须确保所有 key 都传入
KEYS,否则集群模式下会报CROSSSLOT错误
用 EVALSHA 提升性能和降低网络开销
每次发完整脚本字符串既占带宽又增加解析开销。Redis 支持先用 SCRIPT LOAD 缓存脚本,拿到 SHA1 值后,后续用 EVALSHA 执行。
示例流程:
redis-cli SCRIPT LOAD "if redis.call('EXISTS', KEYS[1]) == 1 then ..."
# 返回 "2a14e5c8b9f0..."
之后直接调用:
redis-cli EVALSHA 2a14e5c8b9f0... 1 mycounter
注意:
- 脚本加载后永久存在,除非执行
SCRIPT FLUSH - 客户端需自己缓存 SHA 值,首次加载失败时回退到
EVAL - 集群环境下,所有节点都需加载同一份脚本(或由客户端自动分发)
常见陷阱:超时、错误返回与事务边界
Lua 脚本运行期间会阻塞 Redis 单线程,所以脚本必须快——不能循环太多次,不能调用耗时命令(如 KEYS *),也不能做网络请求。
错误处理要明确:
- 脚本里抛异常(
error("xxx"))会导致整个 EVAL 失败,返回(error) ERR xxx - 不要依赖
pcall捕获所有错误——Redis 对 Lua 的支持有限,某些错误仍会中断 - 脚本里不能使用
redis.pcall来“静默失败”,它只是避免崩溃,但返回值仍是nil或错误结构,需要显式检查 - 没有传统事务的回滚概念:脚本内前面的
redis.call成功了,后面失败也不会撤回
最易被忽略的是 key 生命周期管理:如果脚本里对一个已过期的 key 执行 GET,Redis 会返回 nil,但不会触发惰性删除,此时你得自己判断 nil 是空值还是已过期——最好配合 TTL 或统一用 SET ... NX EX 控制初始状态。











