真正可用的防击穿锁必须绑定锁生命周期、缓存回填原子性与空值兜底三要素;redis-rs 不提供开箱即用分布式锁,需用 lua 脚本在 redis 端原子执行双检+回填,tokio 下须禁用同步路径、显式管理 token 与超时。

不能只靠 SET key value NX PX 就防击穿——Rust 里用 Tokio 调 Redis 实现真正可用的防击穿锁,必须把「锁生命周期」、「缓存回填原子性」和「空值兜底」三者绑死,否则并发查库照样打穿。
为什么 redis-rs 的 lock() 方法不直接可用
redis-rs 官方库本身不提供开箱即用的分布式锁抽象;它只暴露底层命令(如 set、eval)和连接管理。社区常见误用是手写 SET key token NX PX 10 + 简单重试,但这在击穿场景下会失败:
- 多个请求同时发现缓存 miss,全部进入加锁逻辑,但只有第一个能成功 —— 其余请求若直接放弃或降级,就等于放行了击穿
- 没做双检(double-check),即加锁后再次检查缓存是否已被别的协程回填,导致重复查库
- 没写空值缓存,DB 返回 null 时未存
NULL+ 短 TTL,下次请求仍会重走锁流程 -
redis-rs同步模式阻塞 Tokio 事件循环,异步模式又得手动拼 Lua 脚本保证解锁原子性
用 redis::cmd("EVAL") + Lua 实现带双检的防击穿锁
核心是把「查缓存 → 加锁 → 再查缓存 → 查 DB → 回填缓存」整个流程压缩进一个 Lua 脚本执行,由 Redis 单线程保证原子性。Tokio 下推荐用 redis::cmd("EVAL") 异步调用:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- Lua 脚本需接收 3 个 key:
cache_key(业务缓存 key)、lock_key(锁 key)、mutex_key(防击穿专用互斥 key) - 先
EXISTS cache_key,命中则直接返回;未命中则用SET lock_key token NX PX 3000尝试加锁 - 加锁失败?立即
GET cache_key再检一次(双检),避免锁排队期间被别人回填 - 加锁成功?设置
mutex_key并返回 token,业务层据此决定是否查库 - 解锁必须用另一段 Lua:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
示例片段(需配合 tokio 和 redis-rs 异步 client):
let script = redis::Script::new(
"if redis.call('exists', KEYS[1]) == 1 then \
return {1, redis.call('get', KEYS[1])} \
elseif redis.call('set', KEYS[2], ARGV[1], 'nx', 'px', ARGV[2]) then \
return {2, ARGV[1]} \
else \
local v = redis.call('get', KEYS[1]) \
if v ~= false then return {1, v} else return {0} end \
end"
);
let result: Vec<value> = script
.key("user:123")
.key("lock:user:123")
.arg(token)
.arg("3000")
.invoke_async(&mut conn)
.await?;</value>
Token 生成、续期与超时必须由业务层显式控制
Tokio 不提供自动续期机制,而防击穿锁的 lease_time 必须大于 DB 查询 + 缓存写入的最大耗时,否则锁提前过期会导致多个协程同时回填。关键点:
- token 必须是 cryptographically secure 的随机字节(用
rand::thread_rng().gen_bytes(),别用时间戳或 PID) - 业务逻辑中查 DB 前要记录开始时间,执行超时后主动调用 Lua 解锁脚本,避免残留锁
- 不要依赖
blocking_timeout—— Tokio 里阻塞等于卡死整个协程,应改用tokio::time::timeout()包裹锁获取逻辑 - 若使用连接池(如
bb8+redis-rs),确保每个acquire()获取的连接能复用到后续GET/SET,避免跨连接导致双检失效
最易被忽略的是:Lua 脚本里所有 key 必须显式传入,不能拼接字符串;mutex_key 和 cache_key 必须分离,否则空值缓存会干扰锁判断;还有,Tokio runtime 的 max_blocking_threads 若设得太小,redis-rs 同步 fallback 会拖垮调度器——生产环境务必禁用同步路径,只走 query_async。










