GET+SET组合必然击穿缓存,因两次网络往返产生状态窗口,多个请求可能同时在空档期查DB;Lua脚本需原子执行查缓存→加锁→查库→写缓存→返回,且KEYS与ARGV严格分离、key须同slot、锁key隔离并带UUID标识与自动过期。

必须用 Lua 脚本把“查缓存→加锁→查库→写缓存→返回”压进一次 Redis 执行,否则任何客户端侧的两步操作(比如先 GET 再 SET)都会在并发下击穿。
为什么 GET + SET 组合必然击穿
缓存击穿不是 Redis 命令的问题,而是客户端两次网络往返产生的状态窗口。比如 GET user:1001 返回空,接着发 SET user:1001 {...} EX 3600,中间哪怕只有 1ms,多个请求就可能同时卡在这个空档里,全部去查 DB。
常见错误包括:
- 用
SET user:1001 {...} NX EX 3600防重复写,但没解决“谁该去查 DB”的协调问题 - 在 Lua 脚本里拼接 key,例如
'user:'..ARGV[1]—— 集群环境下直接报MOVED错误 - 在脚本里调 HTTP 或做耗时计算,导致 Redis 主线程阻塞超时
EVAL 脚本里 key 和 arg 必须严格分离
Redis 集群靠 key 的 slot 路由,脚本执行前必须能预判目标节点。动态拼接 key(如 'user:'..ARGV[1])会让服务端无法识别 slot,执行时报 MOVED。
正确做法是:
-
KEYS数组只放真实 key 名,且多个 key 必须落在同一 slot(可用{user:1001}强制哈希) -
ARGV只放非 key 参数:过期时间、序列化格式、默认值等 - 所有查缓存、写缓存、加锁操作都用
redis.call('GET', KEYS[1])、redis.call('SET', KEYS[2], '1', 'NX', 'PX', ARGV[1])这类显式调用
加锁必须带唯一标识和自动过期
单纯 SETNX 没有过期机制,进程崩溃后锁永远不释放;而 SETNX + EXPIRE 两步又存在竞态窗口。
关键细节:
- 锁 key 要与业务 key 隔离,比如业务 key 是
user:1001,锁 key 用lock:user:1001 - 锁标识建议用客户端生成的 UUID,防止误删他人锁
- 锁过期时间设 5–10 秒,缓存 TTL 设更长(如 300 秒),避免锁过期但缓存还没写入
- 加锁失败时脚本应返回明确信号(如
'WAIT'),由客户端决定是否重试,不能直接返回空
最易被忽略的是:脚本里不能依赖外部状态做判断,所有分支逻辑(命中、未命中、加锁失败、DB 查询失败)都要返回可区分的状态码或值;另外,集群环境下的 key 命名和 slot 对齐,比逻辑本身更容易出错——一个括号没加对,整个脚本在集群上就跑不起来。


















