必须用Lua脚本——因SETNX无法原子性地加锁并设过期,且重建缓存逻辑在客户端执行易出错;Lua在Redis单线程中全执行或不执行,可原子完成查缓存、加锁、放行决策。

支持,而且必须用 Lua 脚本——单纯 SETNX + 客户端逻辑无法真正解决缓存击穿。
为什么 SETNX 单独用会失败
缓存击穿本质是:热点 key 过期瞬间,大量请求同时发现缓存缺失,全部打到 DB。你用 SETNX 加锁,只是抢到了“谁来重建”的资格,但后续三步——查 DB、写缓存、删锁——完全在客户端执行,中间任意环节出错(比如进程 crash、网络中断、DB 查询超时),锁就丢了,其他请求立刻重入,击穿复现。
更致命的是:SETNX 本身不带过期时间,你再补一句 EXPIRE,这两条命令非原子,存在竞态窗口:SETNX 成功后还没来得及 EXPIRE,进程挂了,锁永远不释放。
-
SETNX只能判断“锁是否存在”,不能同时设值+过期 - 加锁和设置 TTL 分两步,必然有间隙
- 重建缓存过程不在 Redis 端,无法保证原子性
Lua 脚本如何保证原子性
Redis 是单线程执行 Lua 脚本的,整个脚本要么全执行完,要么不执行——这就是唯一能兜住“查缓存 → 未命中则加锁并查库 → 写缓存 → 返回”全流程的方式。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
关键点不是“用了 Lua”,而是把所有决策和状态变更压进一次服务端执行:
- 用
redis.call("GET", KEYS[1])先查缓存,命中直接返回 - 未命中时,用
redis.call("SET", KEYS[2], "1", "NX", "EX", ARGV[1])一步完成加锁+设过期(KEYS[2]是锁 key,和业务 key 隔开) - 加锁成功才返回标记(如
"NEED_REBUILD"),告诉客户端去查 DB;失败则返回nil,让客户端轮询或退避 - 绝不把 DB 查询、序列化、业务校验塞进脚本里——那些必须由客户端做
常见误用:把业务逻辑塞进 Lua
有人把整个“查 MySQL → 序列化 → 写缓存”全写进 Lua,这是危险操作。Redis 执行 Lua 是阻塞式的,一旦脚本里调 HTTP 或 DB,几秒就超时,触发 SCRIPT KILL 或连接堆积,整条 Redis 链路卡死。
正确分工很明确:
- Lua 只做 Redis 端的原子决策:缓存有没有?锁能不能拿?要不要放行?
- DB 查询、空值判断、对象序列化、TTL 计算(比如加随机数防雪崩)——全在客户端
- 客户端拿到
"NEED_REBUILD"后,查 DB,然后用SET key value EX 3600 NX原子写入缓存(注意用NX,避免覆盖别人刚写进去的值)
最容易被忽略的一点:锁 key 和业务 key 必须分离,且锁 key 要带命名空间(比如 "lock:product:10086"),否则脚本里 SET 锁时可能误覆写业务数据;另外,客户端收到 nil(锁争抢失败)后,不能立即重试,要加随机退避,否则所有请求在毫秒级内反复撞锁,反而放大压力。

















