zset本身不防击穿,因其是数据结构而非缓存策略,无过期、锁或兜底逻辑;需通过逻辑过期、分片预热、exists检查+fallback、异步回源等组合策略规避击穿。

为什么ZSet本身不防击穿,但能配合策略规避
ZSet 是数据结构,不是缓存策略。它不自带过期、锁或兜底逻辑,ZINCRBY 或 ZREVRANGE 命令查不到数据时,应用层照样会穿透到 DB。所谓“防止击穿”,本质是把 ZSet 当作**结果缓存池**,而非唯一数据源,关键在读路径的设计。
用「逻辑过期 + 分片 ZSet」替代物理 TTL
直接给整个 leaderboard key 设 EXPIRE 是最危险的做法——一旦过期,所有请求并发回源聚合 Top N,瞬间压垮下游。正确做法是:
- 每个分片 ZSet(如
leaderboard:daily:20260903:shard_0)不设 TTL,靠业务周期自然轮换 - 在 value 中嵌入逻辑过期时间(如 JSON 字段
{"data": [...], "expireAt": 1756892400}),由应用判断是否需刷新 - 刷新动作走异步预热:定时任务提前 5 分钟生成新分片(
leaderboard:daily:20260904:shard_0),旧分片继续服务直到切换时刻 - 避免用
DEL或FLUSH清旧 key,改用ZREMRANGEBYRANK key 0 -1渐进清理,防止阻塞
读请求必须带 fallback,不能依赖 ZSet 存在性
高并发下,ZSet key 可能因运维操作、主从切换或分片未就绪而暂时缺失。此时若直接报错或穿透,就是击穿。应强制要求:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 每次
ZREVRANGE前先EXISTS检查 key,不存在则立即返回默认榜单(如空数组或降级静态榜) - 对「当前日榜」这类强时效场景,fallback 不是空,而是上一日的缓存副本(
leaderboard:daily:20260902:shard_0),保证可用性 - 禁止在读路径中同步调用 DB 聚合——所有回源必须走后台 Job,读接口只允许查缓存或 fallback
- 客户端 SDK 层统一注入 fallback 策略,避免每个业务方重复写 if-else
写入洪峰时,ZSet 分片锁要收口到应用层
虽然 ZINCRBY 是原子的,但多个分片写入后合并读取时,仍可能因某一分片延迟导致榜单不一致,进而触发误判“数据缺失”并回源。这时容易在合并逻辑里埋雷:
- 不要在读取时动态发现某个 shard key 不存在就立刻去 DB 查——这是击穿温床
- 合并前对每个分片加超时控制(如
redis.eval批量 fetch,timeout ≤ 50ms),超时即跳过该分片,用历史数据补位 - 分片路由哈希必须稳定(如
user_id % 16),避免因哈希变更导致旧分片被丢弃、新分片无数据 - 监控
zcard和exists的成功率,比监控 QPS 更早暴露击穿苗头
真正难的不是命令怎么写,而是让所有读请求接受「ZSet 可能暂时不可用」这个事实,并把 fallback、预热、分片生命周期全链路串起来。漏掉任意一环,ZSet 再快也挡不住第一波穿透。










