lua脚本通过将“检查是否存在→查库→写入”压缩为服务端原子操作,彻底消除并发击穿窗口;因redis单线程执行脚本,期间其他命令排队等待,天然阻断多请求同时回源db。

因为 Lua 脚本在 Redis 单线程中完整执行,中间不被任何其他命令打断——它把“读+判+写”压缩成一个不可分割的动作,天然规避了并发击穿。
为什么 GET + SET 组合在并发下会击穿
典型击穿场景:缓存未命中时查 DB 并回填,但两个请求几乎同时发现 key 不存在,都去查库、都执行 SET,造成重复加载或脏写。这不是 Redis 命令本身的问题,而是客户端逻辑跨网络往返导致的状态窗口。
常见错误模式包括:
- 先
GET cache:key得到空值 → 再SET cache:key value EX 60,两步之间有毫秒级空档 - 用
SET cache:key value NX EX 60防重,但没处理“谁来查 DB 回填”的协调逻辑,多个客户端仍可能同时触发回源
这类问题无法靠单个命令解决,MULTI/EXEC 也无济于事——它只排队,不阻塞读,更不提供条件分支。
Lua 脚本如何堵住这个窗口
核心是把“检查是否存在 → 不存在则查库并写入 → 返回结果”整个流程搬到服务端执行。Redis 执行脚本时,其他所有命令(包括其他客户端的 GET)都会排队等待,直到脚本退出。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
实操要点:
- 脚本内必须用
redis.call('EXISTS', KEYS[1])判断,不能依赖外部状态 - 回填逻辑(比如调用
redis.call('HGETALL', 'db:users:123')模拟查库)需提前设计好数据结构,避免脚本里再发网络请求(Lua 不能发 HTTP) - 返回值要明确区分:命中缓存、回填成功、回填失败,方便客户端决定是否降级或重试
- 示例片段:
if redis.call('EXISTS', KEYS[1]) == 1 then return redis.call('GET', KEYS[1]) else local data = redis.call('HGETALL', KEYS[2]) -- 模拟从 hash 加载原始数据 if #data > 0 then redis.call('SET', KEYS[1], cjson.encode(data), 'EX', ARGV[1]) end return data end
KEYS 和 ARGV 分离不当会导致集群失效
脚本看似跑通,一上生产集群就报 MOVED 错误,大概率是 key 名拼接造成的。Redis 集群靠 key 的 slot 决定路由,而 redis.call('GET', 'user:'..ARGV[1]) 这种写法让服务端无法预判目标 slot。
正确做法:
- 所有 key 必须显式传入
KEYS数组,例如EVAL "... " 2 user:123 db:users:123 60 -
ARGV只放非 key 数据:过期时间、序列化格式、默认值等 - 多个 key 必须属于同一 slot,否则得拆脚本或改 key 设计(如加
{user:123}tag 强制同槽) - 本地开发用单节点没问题,但脚本上线前务必在集群环境用
redis-cli --cluster check验证 key 分布
容易被忽略的性能与安全边界
脚本不是万能胶,滥用反而引发雪崩:
- 单个脚本执行超过
lua-time-limit(默认 5 秒)会被SCRIPT KILL中断;若已写入部分数据,不会自动回滚——你得自己用redis.pcall()包裹并判断返回结构,但多数场景不如直接拒绝超时请求 -
redis.call()报错即中断,适合“全有或全无”逻辑;redis.pcall()要手动检查type(res) == "table" and res.err,漏判就会静默失败 - 频繁
EVAL传输大脚本会吃带宽,生产环境应预热:SCRIPT LOAD一次,后续用EVALSHA调用,go-redis 的redis.NewScript就自动帮你做了这层封装
真正难的不是写对语法,而是想清楚:这个逻辑是否真的必须原子?有没有更轻量的替代方案(如布隆过滤器预判、本地缓存双重校验)?一旦脚本成为高频路径,它的 CPU 占用就是 Redis 的瓶颈。










