redis原子操作必须用lua脚本实现,因multi/exec无隔离性,watch需重试且不适用多key;lua中keys与argv须严格分离,禁用非确定性函数,解锁需校验所有权,推荐script load+evalsha。

Redis 本身不支持跨命令的原子性组合,MULTI/EXEC 只能保证命令按序执行,无法防止中间状态被其他客户端读写。真正实现“检查-判断-修改”类复杂逻辑的原子存取,必须用 EVAL 或 EVALSHA 运行 Lua 脚本——它在 Redis 单线程中完整跑完,期间无任何穿插。
为什么 MULTI/EXEC 挡不住并发超卖或重复消费
比如“库存 > 0 才扣减”,拆成 GET inventory:sku123 + DECR inventory:sku123 两步:两次网络往返之间,库存可能已被其他请求改掉。事务不会锁 key,也不隔离读状态。
- 事务队列里命令只是排队执行,不阻塞其他客户端对同一 key 的读写
-
WATCH能做乐观锁,但失败要重试,客户端逻辑变重,且不适用于多 key 依赖场景 - 真实业务中(如秒杀、抢红包、消息队列消费)需要“读+判+写+通知”一气呵成,只有 Lua 脚本能兜住
Lua 脚本里 KEYs 和 ARGVs 必须严格分离
脚本里所有 key 名必须通过 KEYS 数组传入,不能拼接;非 key 数据(如数量、过期时间、用户 ID)走 ARGV。否则集群模式下直接报 MOVED 错误,或触发 EVAL 安全限制被拒绝。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 错例:
redis.call('GET', 'user:'..ARGV[1])—— 集群无法路由,本地模式也可能被禁用 - 对例:
redis.call('GET', KEYS[1]),调用时传EVAL "..." 1 user:123 -
KEYS个数必须和脚本中最大索引一致,少传会报ERR Error running script - 多个 key(如订单+库存+日志)必须同属一个 slot,否则得拆成多个脚本或退到单节点
用 redis.call() 而不是 redis.pcall(),除非你真要自己处理异常
redis.call() 遇错直接中断脚本并抛出错误;redis.pcall() 捕获异常后返回状态表(如 {err="..."} ),你需要手动判 type(res) == "table" and res.err。多数业务不需要这种粒度控制,反而容易漏判。
- 简单条件更新(如“存在则 incr,否则 set 1”)用
redis.call()更直白 - 涉及不确定数据(如
LPOP空列表)才考虑redis.pcall()+ 显式返回值判断 - 脚本里禁止
os.time()、math.random()、io.open()等非确定性操作,否则主从复制不一致
解锁操作必须校验锁拥有者,否则 A 会误删 B 的锁
分布式锁最常翻车的点:加锁用 SET key value NX EX 30 是原子的,但解锁若只 DEL key,就完全没校验所有权。A 执行超时锁自动释放,B 拿到锁,A 结束后 DEL 掉的其实是 B 的锁。
- 正确解锁脚本必须先
GET key对比 value,再DEL,整个过程封装进一个脚本 - 示例:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - 生产环境建议配合
SCRIPT LOAD+EVALSHA,避免每次传输脚本内容,也方便复用与灰度
脚本越短越好,几百行是安全线;超过 5 秒执行时间会被 lua-time-limit 中断,而写了一半的数据无法回滚——所以逻辑要前置校验,别在脚本里做网络请求或大循环。










