Redis缓存穿透、击穿与雪崩:原理解析及实战解决方案

19 阅读7分钟

很高兴你能来阅读,这里我会陆续总结自己的项目经验,编程学习思路即可。首先是对自己编程经验反思,其次希望我的分享对大家有帮助!

一.前言

  • Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解
  • 如果对数据的一致性要求很高,那么就不能使用缓存。在缓存使用的过程中也存在一些典型的问题:缓存击穿、缓存穿透、缓存雪崩

image.png

二.缓存击穿

(1)概念

  • 缓存击穿:大并发集中对某一数据进行访问,并且缓存中不存在访问的数据(第一次访问或者缓存过期),持续的大并发就穿破缓存,直接请求数据库。

(2)如何解决

  • 双重检测锁机制(提升效率)
//ItemService
public Item queryItemById(String itemId){
    Item item = itemCacheHander.getItemFromCache(itemId);
    if(item == null) {
        synchronized (this) {
            item = itemCacheHander.getItemFromCache(itemId);
            if (item == null) {
                item = itemDAO.queryItem(itemId);
                itemCacheHander.addItemToCache(item);
            }
        }
    }
    return item;
}
  • 步骤小结:
  • ①高并发情况下,当第一次访问的时候先从缓存数据库中查询,如果为空
  • ②同步锁锁住
  • ③再次从缓存中查询是否为空(这里只有第一个到达的访问会继续进行下一步,因为缓存中没有)
  • ④第一个到达的访问会从再次判断为空,从数据库中查询,查询完结果放入缓存当中
  • ⑤从第二个当达的访问开始每一个都可以从缓存中查取
  • ⑥从而避免大量访问同时进入导致数据库崩掉的情况。

补充:synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

同步锁的作用是保证 “同一时间只有一个线程能查库重建缓存”,其他线程等待锁释放后,可直接从缓存获取已重建的数据,避免无效回源。


三.缓存穿透

(1)概念

  • 缓存穿透,是指查询一个数据库不存在的数据。首先查询缓存,缓存中不存在则查询数据库,数据查询到的数据依然为空,设置到缓存中也为空;因此后续所有对次数据的查询都会先查询缓存,缓存不存在继而又查询数据库。

  • 案例:(想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。)

(2)如何解决

  • 即使数据库查询为空,也向缓冲中写入非空值

解决缓存穿透的思路就是:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。

  • ItemCacheHandler(缓存帮助类)
/**
 * 存缓存
 * @param item
 */
public void addItemToCacheEx(Item item){
    String json = new Gson().toJson(item);
    //设置过期时间
    redisTemplate.boundValueOps("item‐"+item.getItemId()).set(json,5);
}
  • ItemService
//查询操作
public Item queryItemById(String itemId){
    Item item = itemCacheHander.getItemFromCache(itemId);
    if(item == null) {
        synchronized (this) {
            item = itemCacheHander.getItemFromCache(itemId);
            if (item == null) {   
                item = itemDAO.queryItem(itemId);
                // 如果从数据库查询信息为null,则创建一个对象写入到缓冲,并设置过期时间
                if(item == null) {
                    item = new Item();
                    item.setItemId(itemId);
                    itemCacheHander.addItemToCacheEx(item);
                }else {
                    itemCacheHander.addItemToCache(item);
                }
            }
        }
    }
    return item;
}
  • 步骤小结
  • ①高并发大量访问进来,先从缓存中查取若为空
  • ②同步锁从缓存中再次查如为空
  • ③执行从数据库中查询
  • ④如果从数据库查询信息为null,则创建一个对象写入到缓冲,并设置过期时间
  • ⑤返回结果

四.缓存雪崩

(1)概念

  • 缓存雪崩,是指在某一个时间段,缓存集中过期失效,则大量的并发访问查询都落到了数据库上。
  • 案例:例如马上就要到双十一零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。

(2)解决方案

批量设置缓存时,在基础TTL上叠加随机偏移量(如0-300秒),打散不同Key的过期时间,避免大量Key在同一时间集中过期。这是最基础、最核心的方案,几乎适用于所有电商缓存场景。

  • 实现逻辑:基础TTL(如1小时)+ 随机数(0-5分钟),确保每个Key的过期时间分散在不同时间段。
  • 代码示例
@Service
public class ProductCacheService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private ProductMapper productMapper;

    // 基础过期时间:3600秒(1小时)
    private static final long BASE_TTL = 3600;
    // 随机偏移量范围:0-300秒(5分钟)
    private static final int RANDOM_TTL_RANGE = 300;

    // 批量缓存商品数据(如大促前批量上架)
    public void batchCacheProducts(List<Long> productIds) {
        for (Long productId : productIds) {
            ProductDTO product = productMapper.selectById(productId);
            if (product != null) {
                String redisKey = "product:info:" + productId;
                // 计算随机过期时间
                int randomTTL = new Random().nextInt(RANDOM_TTL_RANGE);
                long totalTTL = BASE_TTL + randomTTL;
                // 存储缓存
                redisTemplate.opsForValue().set(redisKey, product, totalTTL, TimeUnit.SECONDS);
            }
        }
    }
}

五、总结

问题类型核心原理电商典型触发场景触发诱因核心风险
缓存穿透请求查询的Key在缓存(Redis)和数据库中均不存在,导致所有请求直接穿透缓存层,持续冲击数据库1. 黑客用伪造商品ID(如超大随机数、负数ID)批量请求商品详情接口;2. 用户查询不存在的订单号、已删除的店铺ID;3. 爬虫爬取不存在的SKU数据1. 无效请求未被拦截;2. 未对不存在的Key做缓存处理;3. 布隆过滤器未覆盖全量合法Key数据库连接池耗尽、CPU/IO飙升,甚至宕机;正常业务查询受影响
缓存击穿某个被高并发访问的「热点Key」突然失效(过期/误删),瞬间大量并发请求绕过缓存,集中冲击数据库单张表1. 大促期间爆款商品缓存过期;2. 秒杀活动商品缓存被误删除;3. 热点商品库存更新后未同步刷新缓存1. 热点Key设置固定短期TTL;2. 运维操作失误删除热点Key;3. 缓存更新机制缺陷(如先删缓存再更数据库的间隙)数据库单表QPS瞬间超限,表锁/行锁竞争激烈,接口响应延迟飙升,甚至出现超时
缓存雪崩同一时间段内,大量缓存Key集中过期,或Redis集群整体故障(主从切换失败、集群宕机),导致缓存层全面失效,全链路请求穿透至数据库1. 大促前批量上架商品时,缓存设置统一过期时间;2. 午夜零点缓存集中过期(日常维护时批量设置TTL);3. Redis主节点宕机,从节点未及时切换1. 批量缓存Key使用固定TTL,未做随机化处理;2. Redis高可用架构缺失(无哨兵/集群);3. 缓存预热不充分,大促流量突增时缓存未命中;4. 网络故障导致Redis集群不可用数据库全面过载,全链路服务拥堵,系统整体瘫痪,用户无法下单、查询商品