缓存穿透、击穿与雪崩:原理+代码+实战全解析

90 阅读4分钟

缓存穿透、击穿与雪崩:处理方案详解及代码实践

1. 引言

在现代的高并发系统中,缓存是提高性能的关键组件之一。然而,缓存也存在一些常见问题,比如 缓存穿透缓存击穿缓存雪崩。这些问题如果不加以处理,可能会导致系统崩溃或性能严重下降。

本文将深入探讨这三种问题的成因以及解决方案,并通过代码示例帮助你更好地理解和应用这些技术。

2. 缓存穿透

2.1 定义

缓存穿透 是指查询一个既不在缓存也不在数据库中的数据。由于缓存未命中,每次请求都会直接打到数据库上,可能导致数据库压力过大甚至崩溃。

2.2 成因

  • 恶意攻击(如利用不存在的ID频繁查询)
  • 数据被删除但缓存未更新

2.3 解决方案

方案一:布隆过滤器(Bloom Filter)

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于集合中。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;
import com.google.common.hash.PrimitiveSink;

// 示例:使用Guava库创建布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
    (Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8),
    1000000
);

bloomFilter.put("key1");
bloomFilter.mightContain("key1"); // 返回true
bloomFilter.mightContain("key2"); // 可能返回false
方案二:缓存空值(Null Caching)

如果查询结果为空,也可以将该结果缓存一段时间,避免重复查询数据库。

public String getFromCache(String key) {
    String value = redis.get(key);
    if (value == null) {
        // 查询数据库
        value = db.query(key);
        if (value == null) {
            // 缓存空值,防止穿透
            redis.setex(key, 60, "");
        } else {
            redis.setex(key, 60, value);
        }
    }
    return value;
}

3. 缓存击穿

3.1 定义

缓存击穿 是指某个热点数据在缓存中过期后,大量并发请求同时访问数据库,导致数据库瞬时压力剧增。

3.2 成因

  • 热点数据缓存过期
  • 并发量高

3.3 解决方案

方案一:永不过期策略

对于某些热点数据,可以设置为“永不过期”,并通过后台任务定期更新。

// 设置缓存永不超时
redis.set("hot_data_key", hotData);

// 后台线程定时更新
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    String newData = db.queryHotData();
    redis.set("hot_data_key", newData);
}, 0, 5, TimeUnit.MINUTES);
方案二:互斥锁(Mutex Lock)

当缓存失效时,只允许一个线程去加载数据,其他线程等待。

public String getWithLock(String key) {
    String value = redis.get(key);
    if (value == null) {
        synchronized (this) {
            value = redis.get(key);
            if (value == null) {
                value = db.query(key);
                redis.setex(key, 60, value);
            }
        }
    }
    return value;
}

4. 缓存雪崩

4.1 定义

缓存雪崩 是指大量缓存数据在同一时间失效,导致所有请求都落到数据库上,可能造成数据库崩溃。

4.2 成因

  • 所有缓存设置了相同的过期时间
  • 缓存服务器宕机

4.3 解决方案

方案一:随机过期时间

在设置缓存过期时间时,加上一个随机偏移量,避免所有缓存同时失效。

int expireTime = 60 + new Random().nextInt(10);
redis.setex(key, expireTime, value);
方案二:集群部署 + 高可用架构

使用 Redis 集群或主从复制机制,确保即使某一台缓存服务器宕机,也能保证整体服务的可用性。

# Redis Cluster 配置示例
cluster-enabled yes
cluster-node-timeout 5000
方案三:限流降级

在前端加一层限流逻辑,防止短时间内大量请求冲击数据库。

// 使用Guava的RateLimiter进行限流
RateLimiter rateLimiter = RateLimiter.create(1000);

if (rateLimiter.tryAcquire()) {
    // 允许请求
} else {
    // 返回错误或降级响应
}

5. 综合应用场景设计

我们来设计一个电商平台的商品详情页场景,结合以上三种缓存问题的解决方案。

5.1 场景描述

  • 用户访问商品详情页,系统优先从缓存中获取数据
  • 如果缓存中没有,则查询数据库并写入缓存
  • 商品信息变更后,异步更新缓存
  • 热门商品缓存永不过期
  • 所有缓存设置随机过期时间
  • 使用布隆过滤器防止恶意攻击

5.2 实现代码

public class ProductService {

    private final RedisClient redis;
    private final DBClient db;
    private final BloomFilter<String> bloomFilter;

    public Product getProductDetail(String productId) {
        // 1. 检查布隆过滤器
        if (!bloomFilter.mightContain(productId)) {
            return null; // 直接拒绝非法请求
        }

        // 2. 从缓存获取
        Product product = redis.get("product:" + productId);
        if (product == null) {
            synchronized (this) {
                product = redis.get("product:" + productId);
                if (product == null) {
                    // 3. 查询数据库
                    product = db.getProductById(productId);

                    if (product != null) {
                        // 4. 写入缓存,设置随机过期时间
                        int expireTime = 60 + new Random().nextInt(10);
                        redis.setex("product:" + productId, expireTime, product);
                    } else {
                        // 5. 缓存空值
                        redis.setex("product:" + productId, 60, "");
                    }
                }
            }
        }

        return product;
    }
}

6. 总结

缓存穿透、击穿和雪崩是高并发系统中常见的三大缓存问题,合理的设计和实现可以显著提升系统的稳定性和性能。通过布隆过滤器、缓存空值、互斥锁、随机过期时间等手段,我们可以有效应对这些挑战。

希望本文对你理解这些概念有所帮助,欢迎留言交流~