缓存击穿本质是热点key刚失效时高并发读导致瞬时DB压力,需通过逻辑过期、分布式锁与随机TTL偏移组合防护,而非读写分离或硬编码耦合。

缓存击穿的本质是单点穿透,不是并发问题本身
缓存击穿指某个 key 过期瞬间,大量请求同时打到数据库,造成瞬时压力。它和缓存雪崩(大批 key 集中过期)、缓存穿透(查不存在的 key)不同,核心在于「热点 key 刚失效 + 高并发读」。业务代码里硬编码加锁或重试逻辑,会把缓存策略和业务逻辑耦合死——比如在 getUserById() 里塞 tryLock() 和 loadFromDB(),后续换 Redis 为 Caffeine 或加多级缓存时就得改业务方法。
用装饰器模式封装「防击穿」,不碰业务方法体
真正解耦的做法,是把「加载、加锁、回填缓存」抽成可复用的拦截逻辑。Java 可用 @Cacheable 配合自定义 CacheResolver + CacheAspect;Go 可用函数选项模式包装 GetUser():
func WithCacheBreakProtection(fn func() (interface{}, error)) func() (interface{}, error) {
return func() (interface{}, error) {
key := "user:123"
if val, ok := redis.Get(key); ok {
return val, nil
}
// 尝试获取分布式锁(如 SET key value NX PX 3000)
if !redis.TryLock("lock:"+key, "1", 3000) {
return redis.WaitAndRetry(key, 100*time.Millisecond, 5) // 退避重试
}
defer redis.Unlock("lock:"+key, "1")
val, err := fn() // 真正查 DB
if err == nil {
redis.SetEx(key, val, 60)
}
return val, err
}
}
业务侧只写 GetUserFromDB(),再套一层 WithCacheBreakProtection(GetUserFromDB),后续替换锁实现或缓存组件,都不用动业务函数。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
读写分离策略在这里反而会放大击穿风险
很多团队一提「读写分离」就直接上主从 Redis:写走 master,读走 slave。但缓存击穿发生时,所有读请求都可能落到同一个 slave(尤其用了连接池或哈希路由),而 slave 的数据和 master 并非强一致——EXPIRE 在 master 执行后,slave 要等异步复制,这期间 slave 上该 key 可能已过期但还没同步删除指令,导致多个读请求在 slave 上同时 miss,一起打 DB。
- 读写分离不解决 key 过期瞬间的并发问题,只解决写压力
- slave 的过期时间可能比 master 晚几十毫秒,加剧击穿窗口
- 若用
READONLY连接只读节点,SET回填缓存会失败,必须切回 master 连接,引入路由判断开销
真正轻量又有效的业务层防护组合
不需要引入复杂框架或中间件,三件事配齐就能覆盖大部分场景:
- 给热点
key加随机过期偏移:比如基础 TTL 是 60s,实际设为60 + rand.Intn(10),避免集中失效 - 用「逻辑过期」代替物理过期:缓存值里嵌入
expireAt字段,应用层判断是否过期,过期则异步刷新,不阻塞当前请求 - DB 查询前先尝试
SET key lock_value NX PX 3000,成功才查库,失败则等待后重试——这个锁必须带唯一 value(防误删),且超时要短于业务正常响应时间
逻辑过期和分布式锁的 value 冲突、NX 条件下误删其他客户端的锁、随机偏移太小仍可能撞车……这些细节不处理好,防护就形同虚设。

















