【Redis】什么是缓存击穿?

参考笔记:

https://blog.csdn.net/2301_80017072/article/details/149691412https://blog.csdn.net/2301_80017072/article/details/149691412

一、 前言

缓存的使用方式如上图所示:

发起一个查询请求时,首先判断缓存中是否有该数据;

如果缓存中存在,则直接返回数据

如果缓存中不存在,则查询数据库,然后把数据写入到缓存中,最后返回数据

了解了上述过程后,下面说说缓存击穿

二、什么是缓存击穿

缓存击穿(Cache Breakdown)是指某个热点数据过期失效的瞬间,大量并发请求直接穿透缓存层,同时涌入数据库进行查询的现象。这种突发的高负载可能导致数据库瞬间压力激增,引发系统崩溃

缓存击穿案例:

线程1 在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了。但是假设在线程1 没有走完的时候,后续的线程2、线程3、线程4并发访问当前这个方法, 此时这些线程都去缓存中查询数据,都没查到,那么他们就会同一时刻去访问数据库,数据库压力激增。此外,当这些请求查询完成后,都会重复地重建缓存

三、为什么会发生缓存击穿?

根本原因在于热点数据的高并发访问特性

  • 电商首页爆款商品
  • 新闻网站头条内容
  • 秒杀活动核心数据

当这些数据缓存过期时,如果未能及时重建缓存,海量请求会像洪水般冲破缓存防线

四、五大解决方案及代码实现

1. 互斥锁(Mutex Lock) ----- 推荐方案

核心思想 :只允许一个线程重建缓存,其他线程阻塞等待

原理: 锁能实现互斥性。多个并发线程过来,使用互斥锁可以实现不同线程间互斥地访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行

伪代码实现:

java 复制代码
public String getData(String key) {
        // 1. 尝试从缓存获取数据
        String data = redis.get(key);
        if (data != null) return data; // 缓存命中

        // 2. 尝试获取分布式锁
        String lockKey = "lock:" + key;
        if (redis.setnx(lockKey, "1", 30)) { // SETNX实现锁,30为锁的有效期,避免因为服务异常一直不释放锁,产生死锁
            try {
                // 3. 再次检查缓存(防止重复查询)
                data = redis.get(key);
                if (data != null) return data;

                // 4. 查询数据库
                data = db.query(key);
                // 5. 写入缓存(设置合理过期时间)
                redis.setex(key, 3600, data);
            } finally {
                // 6. 释放锁
                redis.del(lockKey);
            }
            return data;
        } else {
            // 7. 未获取锁的线程休眠后重试
            Thread.sleep(100);
            return getData(key); // 递归重试
        }
    }

2. 逻辑过期

核心思想:缓存永不过期,但存储包含时间戳的封装对象

方案分析: 之所以会出现缓存击穿 问题,主要原因是对 Key 设置了过期时间 ,假设不设置过期时间,就不会有缓存击穿问题。但是不设置过期时间就会一直占用内存,所以可以采用逻辑过期方案

逻辑过期方案,即不设置 Key 的过期时间,而是把过期时间设置在 RedisValue 中,后续再通过逻辑去处理。如下所示:

结合具体案例,理解逻辑过期方案:

假设线程1 去查询缓存,然后从 Value 中判断出来当前数据已经过期,此时线程1去 获得互斥锁 ,那么其他线程会进行阻塞,获得锁的 线程1 开启一个新的异步线程 ,即 线程2 去进行 重建缓存数据 的逻辑,直到 线程2 完成这个逻辑后,才 释放互斥锁 , 而 线程1 作为主线程不会被线程2阻塞,直接返回过期的缓存数据。假设 线程2 还未结束,而此时 线程3 过来访问,由于 线程2 持有锁,所以 线程3 无法获得锁, 线程3 也直接返回过期的缓存数据,只有等到 线程2 重建缓存结束后,其他线程才能走返回正确的数据

逻辑过期方案的妙处在于能够异步地重建缓存数据,不会影响查询性能。缺点在于在重建完缓存之前,返回的都是脏数据,存在数据不一致的问题

伪代码:

java 复制代码
// 创建10条线程的线程池,最多10个线程同时跑"重建缓存"
private static final ExecutorService threadPool = Executors.newFixedThreadPool(10); 

// 商铺信息
class Shop{
	Long id;
	String name;
	String image;
	.....;
}

// 封装逻辑过期时间
class CacheData {
    Object data;
    long expireTime; // 逻辑过期时间
}

public String getData(String key) {
    // 1、获取缓存数据,检查逻辑时间是否已过期
    CacheData cacheData = redis.get(key);
    if (cacheData.expireTime >= System.currentTimeMillis()) { // 未过期,直接返回数据
        return cacheData.data;
    }
    
    // 2、已过期,获取互斥锁,完成缓存重建
    String lockKey = "lock:" + key;
    Boolean getLock = redis.setnx(lockKey, "1", 30);
    if(!getLock){ // 获取锁失败,直接返回过期数据
        return ccacheData.data;
    }
    
    // 3、成功获取锁,这里可以做个Double Check(可选),再次检查缓存,避免多线程并发下,重复重建缓存
    cacheData = redis.get(key);
    if (cacheData.expireTime >= System.currentTimeMillis()) { // 未过期,则直接返回数据
        return cacheData.data;
    }
    
    // 4、开异步线程,重建缓存,主线程不会被阻塞
    threadPool.submit(() ->{
        // 数据库查询数据
        Shop newShop = db.query(key);
        
        // 封装逻辑过期时间
        CacheData cacheData = new CacheData(newShop,newExpireTime);
        
        // 写入缓存
        redis.set(key,cacheData);
        
        // 释放锁
        redis.del(lockKey);
        
    });
    
    // 5、主线程返回旧数据
    return cacheData.data; 
}

3. 热点数据永不过期

适用场景:极少变更的静态热点数据(如城市列表)

bash 复制代码
# Redis操作
redis.set(Ket,Value); // 不设置过期时间

4. 其他方案....

补充...

5. 方案对比与建议

方案 优点 缺点 适用场景
互斥锁 * 保证一致性 * 没有额外的内存消耗 * 实现简单 * 存在短暂阻塞风险,查询性能受到影响 * 可能有死锁风险 金融/交易类业务
逻辑过期 * 查询性能强 * 数据一致性弱, * 有额外内存消耗 * 实现复杂 资讯/社交类业务
永不过期 * 实现简单 * 查询性能强 * 内存压力大 极少变更的静态数据
.... ..... ...... ......
相关推荐
Jing_jing_X2 小时前
MCP (一)是什么?一文讲清 AI 如何连接现实世界
数据库·人工智能·oracle
阿凡观察站2 小时前
2026年工程项目管理软件推荐:这5款主流产品值得关注
大数据·数据库·低代码·finebi·简道云
逸Y 仙X2 小时前
文章二十一:ElasticSearch 词项查询与调度查询实战
java·大数据·数据库·elasticsearch·搜索引擎
李李李勃谦2 小时前
鸿蒙PCBI 报表工具:连接数据库与可视化报表生成
数据库·华为·交互·harmonyos
czlczl200209253 小时前
MAX()和MIN()优化
数据库·mysql·性能优化
傻瓜搬砖人4 小时前
SpringBoot整合Junit-Redis-打包
spring boot·redis·junit
014-code4 小时前
布隆过滤器:判断“可能存在“和“一定不存在“
java·redis
gQ85v10Db4 小时前
Redis分布式锁进阶第十八篇:本地缓存+分布式锁双锁架构 + 高并发削峰兜底 + 极致性能无损优化实战
redis·分布式·缓存
消失的旧时光-19434 小时前
SQL 第一篇:CRUD 实战,从 user 表开始写接口
数据库·sql·mysql