当前位置: 首页 > news >正文

面试Redis篇-深入理解Redis缓存击穿

Java开发者必备:深入理解Redis缓存击穿

一、 什么是缓存击穿 (Cache Breakdown)?

1. 核心定义

缓存击穿,也叫“热点Key问题”,指的是某一个访问极其频繁的热点Key,在它过期失效的那一瞬间,有海量的并发请求同时涌入。由于缓存中已无此Key,这些请求会全部“击穿”缓存层,直接打向后端的数据库,导致数据库压力瞬间剧增,甚至崩溃。

它的核心特征是:单一热点Key + 高并发 + 精准地发生在失效的瞬间

2. Java项目中的场景比喻

想象一下,你正在为某款爆款秒杀商品(例如,最新款的iPhone)的详情页做缓存。这个商品是全网关注的焦点(热点Key),它的缓存你设置了10分钟过期。

在晚上8点整,秒杀活动开始,成千上万的用户疯狂刷新商品详情页。很不巧,在8:05:00,这个商品的缓存正好过期了。在8:05:01这一秒内,可能有数千个请求(高并发)同时到达你的服务器。

  • 你的ProductServiceImpl发现Redis中没有这个productId的缓存。
  • 于是,这数千个线程都去执行productMapper.selectById(productId),向数据库发起了数千次完全相同的查询。
  • 数据库瞬间被这一个SQL查询打爆,CPU飙升,连接池耗尽,导致整个商品服务不可用。

缓存击穿就像用一把高能激光(高并发请求)精准地击穿了盔甲(缓存)上一个最薄弱的点(过期的热点Key)。

二、 与缓存雪崩、缓存穿透的清晰辨析

这三个概念非常容易混淆,面试时清晰地辨别它们是加分项。

特性缓存击穿 (Breakdown)缓存雪崩 (Avalanche)缓存穿透 (Penetration)
影响范围单个 Key大量 Key大量不存在的 Key
根本原因单个热点Key过期大量Key同时过期或Redis宕机查询不存在的数据
攻击对象数据库中存在的数据数据库中存在的数据数据库中不存在的数据
形象比喻一束激光,精准打击一点大坝决堤,全面冲击散弹枪,漫无目的扫射

三、 缓存击穿的解决方案

解决缓存击穿的核心思路是:避免在高并发下,让多个线程同时去重建同一个热点Key的缓存

方案一:使用互斥锁/分布式锁 (Mutex Lock / Distributed Lock)

思路:

这是最经典、最常用的解决方案。当缓存失效时,只允许第一个获取到锁的线程去查询数据库和重建缓存,其他线程则等待或直接返回。

Java 实现示例 (使用 Redisson 分布式锁):

在分布式环境下,必须使用分布式锁,而不是Java的synchronized或ReentrantLock。Redisson是优秀的实现。

@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;public Product getProductById(Long id) {String key = "product:" + id;// 1. 先查缓存Object cachedProduct = redisTemplate.opsForValue().get(key);if (cachedProduct != null) {return (Product) cachedProduct;}// 2. 缓存未命中,获取分布式锁RLock lock = redissonClient.getLock("lock:product:" + id);try {// 尝试获取锁,最多等待10秒if (lock.tryLock(10, TimeUnit.SECONDS)) {// 3. 【双重检查】成功获取锁后,再次检查缓存// 防止在高并发下,等待锁的过程中,已有其他线程完成了缓存重建cachedProduct = redisTemplate.opsForValue().get(key);if (cachedProduct != null) {return (Product) cachedProduct;}// 4. 真正去数据库查询并重建缓存Product productFromDb = productMapper.selectById(id);if (productFromDb != null) {redisTemplate.opsForValue().set(key, productFromDb, 30, TimeUnit.MINUTES);} else {// 防止缓存穿透,可以缓存一个空值redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);}return productFromDb;} else {// 未获取到锁的线程,可以稍等后重试Thread.sleep(100);return getProductById(id); // 递归调用重试}} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态return null;} finally {// 5. 确保释放锁if (lock.isLocked() && lock.isHeldByCurrentThread()) {lock.unlock();}}
}

优点:

  • 强一致性:保证了只有一个线程更新缓存,数据一致性高。
  • 思路清晰:逻辑简单明了。

缺点:

  • 性能影响:引入了锁,未获取到锁的线程需要等待,系统吞吐量会下降。
  • 可能死锁:如果锁的实现或使用不当,有死锁风险。

方案二:热点数据永不过期(逻辑过期)

思路:

这是一种以空间换时间、以可用性为先的策略。我们不给Redis中的热点Key设置物理过期时间(TTL),而是在缓存的值中包含一个“逻辑过期时间”字段。

  1. 当请求线程发现数据的逻辑时间已过期时,它不会阻塞。
  2. 它会先立即返回旧的(但可用的)数据给用户。
  3. 同时,它会尝试获取一个锁,并派发一个异步线程去执行缓存重建任务。

Java 实现示例:

首先定义一个包含逻辑过期时间的包装类。

class CacheData<T> {T data;LocalDateTime logicalExpireTime;
}

然后是查询逻辑。

// 假设有一个专门执行刷新任务的线程池
private static final ExecutorService CACHE_REFRESH_EXECUTOR = Executors.newFixedThreadPool(10);public Product getProductWithLogicalExpire(Long id) {String key = "product:" + id;// 1. 从Redis获取数据CacheData<Product> cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(key);// 2. 缓存未命中(说明非热点或首次访问),直接返回null(或查询数据库)if (cacheData == null) {return null; }// 3. 命中缓存,判断是否逻辑过期if (cacheData.getLogicalExpireTime().isAfter(LocalDateTime.now())) {// 3.1 未过期,直接返回数据return cacheData.getData();}// 4. 已逻辑过期,需要重建缓存String lockKey = "lock:product:" + id;// 尝试获取分布式锁if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {// 获取锁成功,开启一个独立线程去重建缓存CACHE_REFRESH_EXECUTOR.submit(() -> {try {// 查询数据库,创建新的CacheData对象,并写入RedisProduct newProduct = productMapper.selectById(id);CacheData<Product> newCacheData = new CacheData<>();newCacheData.setData(newProduct);newCacheData.setLogicalExpireTime(LocalDateTime.now().plusMinutes(30));redisTemplate.opsForValue().set(key, newCacheData);} finally {// 释放锁redisTemplate.delete(lockKey);}});}// 5. 无论是否获取到锁,都立刻返回旧的数据,保证用户体验return cacheData.getData();
}

优点:

  • 高可用性:用户的请求线程几乎不会被阻塞,总能立即拿到数据(即使是旧的)。
  • 性能好:没有大量线程因等待锁而挂起。

缺点:

  • 数据不一致:在缓存重建完成前,用户会拿到旧数据。
  • 实现复杂:需要额外的逻辑和线程池来管理。

四、 Java面试中如何回答“缓存击穿”

面试官您好,对于缓存击穿,我的理解如下:

首先,它的定义和特点是, 缓存击穿是指一个热点Key,在它过期的一瞬间,有大量并发请求过来,导致这些请求都穿透了缓存,直接打到数据库上。它和雪崩(大量Key过期)以及穿透(查询不存在的Key)的关键区别在于,它的攻击目标是单一的、存在的、高热度的Key

其次,针对这个问题,我了解两种主流的解决方案:

第一种是“互斥锁”方案。 这是最经典的解决方案。当缓存失效后,我们不是让所有请求都去查数据库,而是先用一个分布式锁(比如基于Redis的SETNX或使用像Redisson这样的客户端)来保证只有一个线程能去执行数据库查询和缓存重建的任务。其他线程则等待。为了优化性能,获取锁的线程在执行任务前会进行一次双重检查,看缓存是否已经被其他线程重建好了,避免重复工作。这个方案的优点是数据一致性强,缺点是会牺牲一些吞吐量。

第二种是“逻辑过期”方案。 这是一个更侧重于高可用性的高级方案。我们不给热点Key设置物理TTL,而是在Value中存一个逻辑过期时间。当请求发现数据逻辑过期时,它会先立即返回旧数据给用户,保证用户请求不被阻塞,然后异步地派发一个后台线程去真正地更新缓存。这个方案的优点是用户体验极好,几乎无感知,但缺点是在更新期间会存在短暂的数据不一致

最后,关于方案的选择, 我认为需要根据业务场景来权衡。如果业务对数据一致性要求极高,比如金融领域的交易数据,那么“互斥锁”方案更合适。但如果业务场景是像新闻、商品详情页这种,用户体验和高可用性优先,并且能容忍短暂的数据不一致,那么“逻辑过期”方案是更好的选择。

http://www.xdnf.cn/news/1138555.html

相关文章:

  • Selenium 启动的浏览器自动退出问题分析
  • 全面升级!WizTelemetry 可观测平台 2.0 深度解析:打造云原生时代的智能可观测平台
  • 杭州卓健信息科技有限公司 Java 面经
  • web前端渡一大师课 CSS属性计算过程
  • 损失函数的等高线与参数置零的关系
  • 从AWS MySQL数据库下载备份到S3的完整解决方案
  • Linux操作系统之线程:线程概念
  • mongodb-org-mongos : Depends: libssl1.1 (>= 1.1.1) but it is not installable
  • Java使用FastExcel实现Excel文件导入
  • 镁合金汽车零部件市场报告:行业现状、发展趋势与投资前景分析
  • 集群聊天服务器各个类进行详解
  • Docker国内镜像
  • 关于用git上传远程库的一些常见命令使用和常见问题:
  • RuoYi-Cloud 定制微服务
  • Java集合框架中List常见问题
  • 【软件开发】主流 AI 编码插件
  • 服务器数据恢复—raid5磁盘阵列崩溃如何恢复数据?
  • 《每日AI-人工智能-编程日报》--2025年7月17日
  • Odoo最佳业务实践:从库存管理重构到全链路协同
  • Jmeter 性能测试响应时间过长怎么办?
  • 下载anaconda和pycharm,管理python环境
  • Kubernetes 学习笔记
  • 暑期自学嵌入式——Day05(C语言阶段)
  • MyBatis 之配置与映射核心要点解析
  • 三轴云台之测距算法篇
  • 硅谷顶级风投发布《2025年AI实战手册》|附下载
  • 【Elasticsearch】Elasticsearch 快照恢复 API 参数详解
  • 一次多架构镜像构建实战:Docker Buildx + Harbor 踩坑记录
  • arping(ARP协议网络测试工具)
  • ota之.加密算法,mcu加密方式