欢迎光临
我们一直在努力

缓存击穿-深入理解

缓存击穿就是:某个热点 key突然失效了,这一瞬间大量请求一起打到数据库,把数据库冲垮。

  • 方案1:加锁 → 到期后,只允许一个线程查数据库重建缓存;

  • 方案2:逻辑过期 → 到期后,先返回旧数据,再偷偷异步更新缓存。

一、方案1:分布式锁

这套思路是:

缓存没了,不要让所有请求都去查数据库,而是抢锁,谁抢到谁去查库,其他人等着。

你这段代码流程可以拆成 5 步:

1)先查缓存

Article article = redisUtil.get(cacheKey);
if (article != null) {
return article;
}

意思是:

  • Redis 里有数据,直接返回

  • Redis 里没数据,说明可能过期了,准备重建缓存

这是第一层拦截,大多数请求都应该在这里结束。

2)缓存没命中,就抢分布式锁

RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock(3, 10, TimeUnit.SECONDS))

这里 tryLock(3, 10, TimeUnit.SECONDS) 可以这样记:

  • 3:最多等 3 秒去抢锁

  • 10:抢到锁后,锁最多持有 10 秒,防止死锁

作用是:

  • 只有一个线程能进入查数据库的逻辑

  • 其他线程抢不到锁,就别一起冲数据库

3)拿到锁后,要“双重检查”

article = redisUtil.get(cacheKey);
if (article != null) {
return article;
}

为什么还要再查一次缓存?

因为可能你抢锁的这段时间里,别的线程已经重建好缓存了。

比如:

  • 线程A先拿到锁,查库并写入缓存

  • 线程B后面才拿到锁

  • 如果线程B不再查缓存,就会又查一次数据库,浪费

所以这个叫双重检查。

4)再查数据库并写回缓存

article = articleMapper.selectById(id);
redisUtil.set(cacheKey, article, 1, TimeUnit.HOURS);

这个步骤就是缓存重建:

  • 从数据库查文章

  • 重新放进 Redis

  • 设置过期时间 1 小时

这样后面的请求就都能走缓存了。

5)释放锁

if (lock.isHeldByCurrentThread()) {
lock.unlock();
}

必须判断是不是当前线程持有锁,再解锁。

因为:

  • 有可能你没抢到锁

  • 有可能锁超时自动释放了

  • 有可能锁被别的线程持有

如果乱解锁,会出问题。

6)没抢到锁怎么办?

Thread.sleep(100);
return getHotArticle(id);

你的代码这里是:

  • 没抢到锁,先睡 100 毫秒

  • 然后递归重试

意思是:等抢到锁的线程把缓存建好,再来查一次。

方案1的核心思想

你要记住这句话:

缓存失效后,通过分布式锁保证只有一个请求查库,其他请求等待缓存重建完成。

二、方案2:逻辑过期 + 异步更新

这套思路是:

热点数据不要真正过期,而是给它加一个“逻辑过期时间”;过期了也先返回旧数据,同时异步更新。

这个方案更适合热点数据,因为热点数据最怕瞬间大量请求一起打库。


1)缓存里存的不是单纯 Article,而是包装对象

CacheWrapper wrapper = redisUtil.get(cacheKey);

这里缓存的数据结构是:

{
data: 文章数据,
expireTime: 逻辑过期时间
}

意思是:

  • data:真正的数据

  • expireTime:业务自己定义的“过期时间”

注意:

这里的“过期”不是 Redis 真正删掉 key,
而是你代码里自己判断“这个数据该不该更新了”。

2)如果缓存不存在,就同步加载

if (wrapper == null) {
return loadAndCache(id);
}

说明这是第一次访问,Redis 里还没有数据,那就正常查库并写缓存。

这个没啥特别的。

3)如果逻辑过期了,就异步更新

if (wrapper.getExpireTime() < System.currentTimeMillis()) {
CompletableFuture.runAsync(() -> loadAndCache(id));
}

意思是:

  • 发现这条缓存“过期了”

  • 但是不让当前用户等

  • 直接开一个异步线程去更新缓存

这样,当前请求仍然可以很快返回。

4)当前请求先返回旧数据

return wrapper.getData();

这是这个方案最核心的地方:

即使数据过期了,也不马上让用户卡住,而是先把旧数据返回,保证服务可用。

所以它牺牲一点点实时性,换来高并发下的稳定性。

方案2的核心思想

你记住这句话就行:

逻辑过期不是让缓存真的失效,而是过期后先返回旧数据,再异步重建缓存。

三、这两种方案怎么区分使用

你可以这样记:

用分布式锁的场景

特点是:数据必须尽量新,不能随便返回旧值

比如:

  • 库存

  • 优惠券剩余数量

  • 用户权限信息

因为这些数据一旦旧了,业务可能出错。

用逻辑过期的场景

特点是:允许短时间旧数据,但系统抗压更重要

比如:

  • 热门文章

  • 商品详情

  • 首页推荐

  • 热榜数据

这些数据晚几秒更新,用户通常能接受。

四、你可以这样理解两者区别

方案1:分布式锁

像一家店只有一个补货员:

  • 货架空了

  • 只允许一个人去仓库拿货

  • 其他人先等着

优点是货一补上就是新的。
缺点是大家要等。

方案2:逻辑过期

像货架上先放“昨天的展示样品”:

  • 即使真正新货还没补上

  • 客人也能先看旧样品

  • 店员后台慢慢补新货

优点是客人不用等。
缺点是看到的可能不是最新的。

五、你该怎么学会写这两种代码

你可以按这个模板去记。

分布式锁版模板

public Article getHotArticle(Long id) {
// 1. 查缓存
// 2. 未命中则抢锁
// 3. 抢到锁后双重检查缓存
// 4. 查数据库
// 5. 回写缓存
// 6. 释放锁
// 7. 没抢到锁则等待重试
}

核心点就 4 个字:

查、锁、查库、回填

其中最容易漏的是:

  • 双重检查

  • finally 解锁

  • 空值缓存

  • 不要无限递归

逻辑过期版模板

public Article getHotArticle(Long id) {
// 1. 查缓存包装对象
// 2. 没有就同步加载
// 3. 有的话判断逻辑过期时间
// 4. 过期则异步重建
// 5. 当前请求返回旧值
}

核心点就 4 个字:

旧值兜底

最容易漏的是:

  • 缓存里要存 expireTime

  • 异步更新最好加锁

  • 适合允许旧数据的业务

六、面试里怎么简洁回答

你可以直接背这段:

处理缓存击穿常见有两种方式。第一种是分布式锁:缓存失效后,通过 Redisson 锁保证只有一个线程查数据库并重建缓存,其他线程等待,这样可以防止大量请求同时打到数据库。第二种是逻辑过期:缓存数据本身不立即删除,而是额外存一个逻辑过期时间,发现过期后先返回旧数据,再异步更新缓存,这样可用性更高,适合热点数据场景。

七、最后帮你总结成一句最容易记的

  • 分布式锁:一个人查库,其他人等

  • 逻辑过期:先返回旧数据,后台偷偷更新

赞(0)
未经允许不得转载:171主机测评 » 缓存击穿-深入理解
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址