Redis缓存击穿与雪崩问题诊断:布隆过滤器与互斥锁实战方案

Redis缓存击穿问题诊断与现象识别

Redis缓存击穿是指某个热点key在过期的瞬间,大量并发请求同时穿透缓存直接打到数据库,导致数据库瞬间压力激增甚至连接池耗尽。与缓存穿透(查询不存在的key)和缓存雪崩(大量key同时过期)不同,缓存击穿针对的是单个热点key的并发访问场景。

典型的缓存击穿场景:电商系统中商品详情页的缓存key在过期时恰好遇到促销活动的流量峰值,数百个请求同时发现缓存失效,全部涌向数据库执行相同的查询SQL。数据库连接池在几秒内被占满,后续请求超时,最终触发服务降级。

诊断缓存击穿的方法:检查Redis监控中特定key的miss率是否在某个时间点突然飙升;查看数据库慢查询日志中是否出现大量相同的SQL在短时间内重复执行;监控数据库连接池的使用率是否突然达到上限。

互斥锁(Mutex)解决方案实现

互斥锁的核心思路是当缓存未命中时,只允许一个请求去查询数据库并重建缓存,其他请求等待或返回旧数据。Redis的SETNX命令天然适合实现分布式互斥锁:

public String getProductInfo(String productId) {
    String cacheKey = "product:info:" + productId;
    String lockKey = "lock:product:" + productId;

    // 1. 查询缓存
    String cached = redis.get(cacheKey);
    if (cached != null) {
        return cached;
    }

    // 2. 缓存未命中,尝试获取互斥锁
    // SET lockKey requestId NX EX 10
    String requestId = UUID.randomUUID().toString();
    boolean locked = redis.set(lockKey, requestId, "NX", "EX", 10);

    if (locked) {
        try {
            // 双重检查:获取锁后再次查询缓存
            cached = redis.get(cacheKey);
            if (cached != null) {
                return cached;
            }

            // 3. 查询数据库
            Product product = productMapper.selectById(productId);
            String json = JSON.toJSONString(product);

            // 4. 写入缓存(设置随机过期时间避免雪崩)
            int expire = 300 + ThreadLocalRandom.current().nextInt(60);
            redis.setex(cacheKey, expire, json);

            return json;
        } finally {
            // 5. 释放锁(使用Lua保证原子性)
            String script =
                "if redis.call('get', KEYS[1]) == ARGV[1] " +
                "then return redis.call('del', KEYS[1]) " +
                "else return 0 end";
            redis.eval(script, Collections.singletonList(lockKey),
                       Collections.singletonList(requestId));
        }
    } else {
        // 6. 未获取到锁,短暂等待后重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return getProductInfo(productId); // 递归重试
    }
}

互斥锁方案有几个关键点:lockKey的过期时间设置为10秒,防止持有锁的线程异常退出导致死锁;释放锁使用Lua脚本对比requestId后删除,避免误删其他线程的锁;获取锁后执行双重检查,避免在等待锁期间缓存已被其他线程重建。

互斥锁方案的缺点是增加了请求的等待时间。在高并发场景下,等待锁的请求会堆积,可能触发线程池满。对于响应时间敏感的场景,可以设置最大重试次数,超过后返回降级数据。

布隆过滤器配置与缓存穿透防护

布隆过滤器用于解决缓存穿透问题——查询不存在的key时缓存和数据库都没有数据,每次请求都打到数据库。布隆过滤器通过位图判断元素”可能存在”或”一定不存在”,空间效率远高于HashSet。

// 使用Redisson的布隆过滤器
public class BloomFilterService {

    private RBloomFilter<String> productFilter;

    public void initBloomFilter() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setDatabase(0);

        RedissonClient client = Redisson.create(config);
        productFilter = client.getBloomFilter("product:bloom");

        // 初始化:预计元素数量100万,误判率0.01%
        productFilter.tryInit(1_000_000L, 0.0001);

        // 加载所有商品ID到布隆过滤器
        List<String> productIds = productMapper.selectAllIds();
        for (String id : productIds) {
            productFilter.add(id);
        }
    }

    public String getProduct(String productId) {
        // 布隆过滤器前置检查
        if (!productFilter.contains(productId)) {
            // 一定不存在,直接返回空
            return null;
        }

        String cacheKey = "product:info:" + productId;
        String cached = redis.get(cacheKey);
        if (cached != null) {
            return cached;
        }

        // 查询数据库并更新缓存
        Product product = productMapper.selectById(productId);
        if (product == null) {
            // 数据库也不存在,缓存空值防止穿透
            redis.setex(cacheKey, 60, "");
            return null;
        }

        redis.setex(cacheKey, 300, JSON.toJSONString(product));
        return JSON.toJSONString(product);
    }
}

tryInit方法的两个参数:expectedInsertions是预计要插入的元素数量,falseProbability是可接受的误判率。误判率越低,位图需要的位数越多。100万元素、0.01%误判率需要约1.8MB内存。

布隆过滤器的局限性:存在误判率,不存在的key有极小概率被判断为存在(假阳性),但存在的key不会被判断为不存在(无假阴性)。不支持删除操作,当商品下架后布隆过滤器中的记录无法移除。如果需要删除功能,使用Counting Bloom Filter变体。

缓存雪崩预防与多级缓存架构

缓存雪崩是指大量key在同一时间过期,导致请求全部打到数据库。预防方法是给过期时间添加随机值,避免同时失效:

// 过期时间 = 基础时间 + 随机偏移
int baseExpire = 3600; // 1小时
int randomOffset = ThreadLocalRandom.current().nextInt(300); // 0-5分钟
redis.setex(cacheKey, baseExpire + randomOffset, value);

多级缓存架构通过在Redis之上增加本地缓存层,进一步降低Redis压力和请求延迟:

public class MultiLevelCache {

    // L1: 本地缓存(Caffeine)
    private Cache<String, String> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(60, TimeUnit.SECONDS)
        .recordStats()
        .build();

    // L2: Redis分布式缓存
    @Autowired
    private RedisTemplate<String, String> redis;

    public String get(String key) {
        // L1查找
        String value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }

        // L2查找
        value = redis.opsForValue().get(key);
        if (value != null) {
            // 回填L1
            localCache.put(key, value);
            return value;
        }

        // 数据库查找
        value = dbQuery(key);
        if (value != null) {
            // 同时写入L1和L2
            localCache.put(key, value);
            redis.opsForValue().set(key, value, 300, TimeUnit.SECONDS);
        }

        return value;
    }

    public void evict(String key) {
        localCache.invalidate(key);
        redis.delete(key);
    }
}

L1本地缓存使用Caffeine,过期时间设置较短(60秒),减少本地内存占用。L2 Redis缓存过期时间设置较长(300秒),作为本地缓存失效后的后备。数据库查询只在L1和L2都未命中时触发。

监控指标与告警配置

Redis缓存运维需要持续监控以下核心指标:

# Prometheus + Grafana监控配置示例
# redis_exporter关键指标
redis_connected_clients        # 当前连接数
redis_used_memory              # 已用内存
redis_keyspace_hits_total      # 缓存命中次数
redis_keyspace_misses_total    # 缓存未命中次数

# Grafana告警规则
# 1. 命中率低于90%告警
(hit_rate = hits / (hits + misses)) < 0.90

# 2. 内存使用超过80%告警
used_memory / maxmemory > 0.80

# 3. 连接数超过最大连接数的80%
connected_clients / maxclients > 0.80

# 4. 慢日志告警
slowlog_len > 10

命中率是衡量缓存策略有效性的核心指标,低于90%说明缓存策略需要调整。可能的原因包括过期时间设置过短、缓存key设计不合理导致频繁失效、缓存预热不充分。

Redis缓存击穿、穿透和雪崩是三种不同的缓存异常场景,对应的解决方案也各不相同。互斥锁解决热点key击穿、布隆过滤器解决不存在key穿透、随机过期时间解决批量key雪崩。在生产环境中,这三种方案通常组合使用,配合多级缓存架构和完善的监控告警体系,构建稳定可靠的缓存层。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ji-chuan-yu-xue-beng-wen-ti-zhen-duan-bu/

(0)
小编小编
上一篇 19小时前
下一篇 19小时前

相关推荐