互斥锁(如redis的setnx)是解决缓存重建时多请求击穿数据库的核心方案:仅一个线程加锁后查库并重建缓存,其余线程等待或重试,锁需分离业务key、设超时、避免死锁与惊群效应。

缓存重建时多个请求同时打到数据库怎么办
这是雪崩最典型的连锁反应:一个 key 过期,100 个并发请求都发现 redis.get(key) 返回 null,全去查数据库,瞬间压垮 DB。核心不是“要不要重建”,而是“谁来建、怎么建、其他人怎么等”。
- 用
setnx加锁是最常用手段,但注意:锁 key 要和业务 key 分离(比如"lock:product:123"),避免锁竞争影响其他 key - 锁必须设超时时间(
EXPIRE或SET key val EX seconds NX),否则线程崩溃或网络中断会导致死锁 - 获取锁失败后,不要立即重试——应带随机退避(如
Thread.sleep(10 + random.nextInt(50))),否则所有等待线程会在同一毫秒争抢,形成“惊群效应” - 不建议递归调用自身(如伪代码里
return get_data(key)),容易栈溢出;改用循环重试 + 最大重试次数限制更安全
重建期间用户请求卡住几秒,体验太差怎么办
同步加锁重建本质是“阻塞式兜底”,在高并发下会放大延迟。更优解是让重建异步化,同时保证用户几乎无感。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 查缓存未命中时,先触发异步任务(如线程池提交或发 MQ 消息)去重建,当前请求直接返回旧值(如果存在)或降级数据(如空列表、默认文案)
- 关键前提是缓存 value 中要嵌入逻辑过期时间字段(例如 JSON 里加
"expireAt": 1720846260),而不是依赖 Redis 的物理EXPIRE - 这样即使物理 key 还在,也能判断是否该更新;而 Redis 层不设 TTL,彻底规避集中过期风险
- 异步任务完成后,用
set覆盖旧值即可,无需删再设——减少一次网络往返
为什么不能只靠“错峰设置过期时间”就完事
随机过期时间(如 3600 + random.randint(0, 600))确实能缓解雪崩,但它治标不治本,且有隐蔽陷阱。
- 它只对“自然过期”有效;一旦发生 Redis 故障重启、集群 failover、运维清空缓存等场景,所有 key 物理消失,随机性完全失效
- 冷启动问题:新上线服务或扩容节点后,本地缓存/Redis 都为空,此时随机过期毫无意义,第一波请求仍全部击穿
- 业务语义冲突:某些数据必须严格准时刷新(如每小时汇率),硬加随机会导致数据陈旧超标,反而引发资损
- 真正健壮的方案,是把随机过期当作“日常防护”,配合预热(
preload_hot_data())、多级缓存(本地Caffeine+ Redis)、熔断(Sentinelfallback)一起用
本地缓存和 Redis 双写不一致时怎么取舍
引入本地缓存(如 Caffeine)做二级缓存,目标是降低 Redis 压力,但双写必然带来一致性挑战。
- 优先采用“读多写少”策略:本地缓存只读,写操作只触达 Redis,然后通过消息或 Canal 订阅 Binlog 清掉本地对应 key(
cache.invalidate("user:1001")) - 避免本地缓存设长 TTL;推荐用
expireAfterWrite(2, TimeUnit.MINUTES),靠时效性兜底,而非强一致 - 当 Redis 不可用时,本地缓存可降级为“只读副本”,但需明确标注数据可能滞后(如加
"stale": true字段),前端据此灰度展示 - 切忌在本地缓存里存复杂对象并频繁修改——序列化/反序列化开销可能抵消掉性能收益










