Redis 缓存击穿、穿透、雪崩:概念、成因与解决方案全解析

在构建高性能应用系统时,Redis 缓存凭借其出色的读写速度,已成为减少数据库压力、提升系统响应性能的标配组件。但在实际生产环境中,我们可能会遭遇缓存击穿、穿透、雪崩这三大棘手问题,它们如同隐藏在暗处的 “性能杀手”,稍有不慎就会让系统性能大打折扣,甚至引发服务崩溃。本文将深入剖析这三个问题的本质,通过丰富的案例和直观的图示,为你呈现全面且实用的解决方案。        

一、缓存击穿(Cache Breakdown)​

1.1 定义​

缓存击穿指的是在高并发场景下,某个热点 Key(被大量频繁访问的数据)在缓存中的数据过期瞬间,大量并发请求同时穿透缓存,直接涌向数据库,导致数据库瞬间承受巨大压力,可能出现性能瓶颈甚至崩溃的情况。​

1.2 案例分析​

以电商平台为例,某款热门手机在促销期间成为热点商品,其商品详情数据缓存在 Redis 中。假设该缓存 Key 设置了 1 小时的过期时间,当这 1 小时到期,缓存中的数据失效。此时,恰好处于促销活动的流量高峰,大量用户同时刷新商品详情页,这些请求发现缓存未命中,便一股脑地去数据库查询该商品信息。由于数据库处理能力有限,面对突如其来的高并发查询,很可能因资源耗尽而响应缓慢,甚至无法响应,导致用户体验极差,页面加载长时间转圈或直接报错。

1.4 解决方案​

  • 互斥锁(Mutex):​
  • 原理:当缓存未命中时,使用分布式锁(如 Redis 的 SETNX 命令),只有获取到锁的线程能够去数据库查询数据并更新缓存,其他线程处于等待状态。待获取锁的线程完成数据库查询和缓存更新后,释放锁,其他等待线程再从缓存获取数据。​
  • 示例代码(Java + Redis):​

TypeScript复制

// 尝试获取锁​

Boolean success = jedis.set(key, value, "NX", "EX", 10);​

if (success) {​

try {​

// 获取锁成功,查询数据库​

Object data = queryFromDB(key);​

// 更新缓存​

jedis.set(key, data);​

} finally {​

// 释放锁​

jedis.del(key);​

}​

} else {​

// 未获取到锁,等待一段时间后重试​

Thread.sleep(100);​

return getFromCache(key);​

}​

  • 热点数据永不过期:​
  • 原理:对于一些访问频率极高、数据更新频率较低的热点数据,如商品分类信息、系统配置参数等,设置其缓存永不过期。但需要注意,虽然缓存数据不过期,我们仍需通过其他机制(如数据库变更监听、定时任务等)来保证缓存数据与数据库数据的一致性。​
  • 示例代码(Redis 命令):​

TypeScript复制

# 设置热点数据,不设置过期时间​

SET hot_key "hot_value"​

  • 提前续约缓存:​
  • 原理:在缓存 Key 快要过期时,提前开启一个异步线程或定时任务,重新查询数据库获取最新数据并更新缓存,让缓存 Key 的有效期得到延续,避免在高并发期间出现缓存过期的情况。​
  • 示例代码(Java + Quartz 定时任务):​

TypeScript复制

// 配置Quartz定时任务,在缓存过期前30秒续约​

JobDetail jobDetail = JobBuilder.newJob(RenewCacheJob.class)​

.usingJobData("cacheKey", "hot_key")​

.build();​

SimpleTrigger trigger = TriggerBuilder.newTrigger()​

.startAt(DateBuilder.futureDate(30, DateBuilder.IntervalUnit.SECOND))​

.build();​

scheduler.scheduleJob(jobDetail, trigger);​

在上述代码中,RenewCacheJob 类负责从数据库查询最新数据并更新 Redis 缓存。​

二、缓存穿透(Cache Penetration)​

2.1 定义​

缓存穿透是指查询的数据在缓存和数据库中均不存在,导致每次请求都绕过缓存直接访问数据库。若此类请求频繁发生,会给数据库带来极大压力,甚至可能导致数据库因不堪重负而崩溃。这种情况常见于恶意攻击场景,攻击者故意构造大量不存在的 Key 进行请求,也可能是由于业务代码逻辑漏洞,导致应用程序频繁查询不存在的数据。​

2.2 案例分析​

还是以电商平台为例,假设攻击者通过编写脚本,大量发送查询商品 ID 为负数(如 - 1、-2 等)的请求。由于正常业务中商品 ID 都是正整数,这些负数 ID 在缓存和数据库中都不存在。每次请求到达系统后,缓存未命中,接着去数据库查询,同样无法找到对应数据。随着这类无效请求数量不断增加,数据库的资源被大量消耗,最终可能导致数据库服务不可用,影响正常用户的业务操作。

图中可以看出,由于请求的数据在缓存和数据库中都不存在,请求不断地穿透缓存,持续对数据库发起无效查询。​

2.4 解决方案​

  • 缓存空值:​
  • 原理:当数据库查询结果为空时,将空值(如 null 或特定占位符)存入 Redis 缓存,并设置一个较短的过期时间(如 5 分钟)。后续针对相同 Key 的请求,直接从缓存中获取到空值,避免再次查询数据库。​
  • 示例代码(Java + Redis):
  • Object data = queryFromDB(key);
    if (data == null) {
        // 缓存空值,设置过期时间5分钟
        jedis.setex(key, 300, "null");
        return null;
    } else {
        // 缓存有效数据
        jedis.setex(key, 3600, data);
        return data;
    }

  • 布隆过滤器(Bloom Filter):​
  • 原理:布隆过滤器是一种基于哈希的数据结构,它可以高效地判断一个元素是否存在于一个集合中。在系统初始化时,将数据库中已有的数据 Key 添加到布隆过滤器中。当有请求到达时,先通过布隆过滤器判断该 Key 是否可能存在于数据库中。如果布隆过滤器判断 Key 不存在,那么可以直接返回,无需查询缓存和数据库;如果判断 Key 可能存在,再进行常规的缓存和数据库查询流程。需要注意的是,布隆过滤器存在一定的误判率,但在合理设置参数的情况下,误判率可以控制在很低的水平。​
  • 示例代码(Java + Guava 布隆过滤器):
  • // 创建布隆过滤器,预计元素数量1000000,误判率0.01​
    BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000000, 0.01);​
    // 假设从数据库中获取所有商品ID并添加到布隆过滤器​
    List<String> productIds = queryAllProductIdsFromDB();​
    for (String id : productIds) {​
        bloomFilter.put(id);​
    }​
    // 处理请求时先通过布隆过滤器判断​
    if (!bloomFilter.mightContain(requestKey)) {​
        return null;​
    }

  • 参数校验:​
  • 原理:在请求进入业务逻辑层之前,对请求参数进行严格校验。例如,对于商品 ID,确保其为合法的正整数;对于用户名,检查其是否符合特定的格式要求等。通过这种方式,在源头拦截非法请求,避免无效请求穿透到缓存和数据库。​
  • 示例代码(Spring Boot + Hibernate Validator):
  • @RestController
    @RequestMapping("/products")
    public class ProductController {
    
        @GetMapping("/{id}")
        public ResponseEntity<Product> getProduct(@PathVariable @Valid @Min(1) Long id) {
            // 业务逻辑,查询商品
        }
    }

    在上述代码中,@Min (1) 注解确保传入的商品 ID 是大于等于 1 的正整数,若不符合要求,Spring Boot 会自动返回错误信息,不会进入后续的缓存和数据库查询流程。​

    三、缓存雪崩(Cache Avalanche)​

    3.1 定义​

    缓存雪崩是指在某一时刻,大量缓存数据同时过期失效,导致大量请求瞬间涌向数据库,使数据库承受巨大压力,可能引发数据库性能急剧下降甚至宕机,进而影响整个系统的正常运行。这种情况通常是由于缓存数据的过期时间设置不合理,或者 Redis 服务器出现故障导致缓存数据全部丢失所引起的。​

    3.2 案例分析​

    比如电商平台举办限时抢购活动,为了提升性能,将参与活动的商品信息全部缓存到 Redis 中,并设置了相同的过期时间(假设为活动结束时间)。当活动结束的瞬间,所有这些商品的缓存同时过期,大量用户在活动结束后仍在查询商品信息,这些请求纷纷绕过缓存,直接查询数据库。由于请求量远远超出数据库的正常承载能力,数据库可能会因为资源耗尽而无法正常响应,导致整个平台出现卡顿、报错等问题,严重影响用户体验。

  • 3.4 解决方案​

  • 设置随机过期时间:​
  • 原理:在设置缓存过期时间时,不使用固定的过期时长,而是在一个基础过期时间上加上一个随机值。例如,原本设置缓存过期时间为 1 小时,现在设置为 50 分钟到 70 分钟之间的随机值。这样可以避免大量缓存数据在同一时刻集中过期,使缓存过期时间分散开来,降低数据库瞬间承受高并发请求的风险。​
  • 示例代码(Redis 命令):
  • # 设置缓存,过期时间为60分钟左右的随机值(50 - 70分钟)

    SET key value EX $(($RANDOM % 1200 + 3000))

  • 缓存预热:​
  • 原理:在系统上线或重大活动开始前,提前将热点数据加载到 Redis 缓存中,并设置较长的过期时间。这样在业务高峰期来临时,大部分请求可以直接从缓存获取数据,避免因缓存未命中而大量查询数据库。缓存预热可以通过定时任务、数据初始化脚本等方式实现。​
  • 示例代码(Java 定时任务预热缓存):
  • // 配置定时任务,在系统启动后10分钟开始预热缓存
    JobDetail jobDetail = JobBuilder.newJob(PreheatCacheJob.class)
           .startAt(DateBuilder.futureDate(10, DateBuilder.IntervalUnit.MINUTE))
           .build();
    SimpleTrigger trigger = TriggerBuilder.newTrigger()
           .build();
    scheduler.scheduleJob(jobDetail, trigger);

    在 PreheatCacheJob 类中,实现从数据库查询热点数据并缓存到 Redis 的逻辑。​

  • 多级缓存架构:​
  • 原理:采用本地缓存(如 Guava Cache、EhCache)与 Redis 分布式缓存相结合的多级缓存架构。当请求到达时,先查询本地缓存,如果本地缓存命中则直接返回数据;若本地缓存未命中,再查询 Redis 缓存。若 Redis 缓存也未命中,才查询数据库,并将查询结果依次存入 Redis 缓存和本地缓存。通过这种方式,即使 Redis 缓存出现大量数据过期或故障,本地缓存仍能在一定程度上拦截部分请求,减轻数据库的压力。​
  • 示例代码(Java + Guava Cache + Redis):
  • // 创建Guava本地缓存
    LoadingCache<String, Object> localCache = CacheBuilder.newBuilder()
           .maximumSize(1000)
           .expireAfterWrite(10, TimeUnit.MINUTES)
           .build(new CacheLoader<String, Object>() {
                @Override
                public Object load(String key) throws Exception {
                    // 本地缓存未命中,查询Redis缓存
                    Object data = jedis.get(key);
                    if (data!= null) {
                        return data;
                    }
                    // Redis缓存也未命中,查询数据库
                    data = queryFromDB(key);
                    if (data!= null) {
                        // 将数据存入Redis缓存
                        jedis.setex(key, 3600, data);
                    }
                    return data;
                }
            });
    // 获取数据,先从本地缓存查询
    Object data = localCache.get(key);
    return data;

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值