Redis缓存雪崩与击穿防护实战:多级缓存架构设计与熔断降级方案

Redis作为缓存层承担了数据库前端的绝大部分读取压力。当缓存大面积失效(雪崩)、热点Key过期(击穿)或恶意请求穿透(穿透)时,流量直接打到数据库导致服务雪崩。本文从多级缓存架构设计、缓存策略配置到熔断降级实现,梳理高可用缓存系统的工程方案。

缓存雪崩的根因与防护方案

缓存雪崩指大量Key在同一时间过期,或Redis节点宕机导致缓存集体失效,所有请求打到数据库。核心防护策略是过期时间随机化和服务降级。

// 过期时间随机化——避免同时失效
public class CacheUtil {
    
    private static final int BASE_EXPIRE = 3600;  // 基础过期时间1小时
    private static final int RANDOM_RANGE = 600;  // 随机偏移量10分钟
    
    public static int getRandomExpire() {
        return BASE_EXPIRE + ThreadLocalRandom.current().nextInt(RANDOM_RANGE);
    }
    
    // 批量设置缓存时使用随机过期时间
    public <T> void batchSet(Map<String, T> dataMap) {
        dataMap.forEach((key, value) -> {
            redisTemplate.opsForValue().set(
                key, value, 
                getRandomExpire(), 
                TimeUnit.SECONDS
            );
        });
    }
}

Redis集群宕机场景下的降级策略——本地缓存兜底。使用Caffeine作为二级缓存,Redis不可用时自动切换到本地缓存:

// 多级缓存实现
@Service
public class MultiLevelCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // Caffeine本地缓存
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(Duration.ofSeconds(300))
        .recordStats()
        .build();
    
    private final AtomicBoolean redisAvailable = new AtomicBoolean(true);
    
    public <T> T get(String key, TypeReference<T> type, Supplier<T> dbLoader) {
        // 1. 先查本地缓存
        Object localValue = localCache.getIfPresent(key);
        if (localValue != null) {
            return (T) localValue;
        }
        
        // 2. 查Redis缓存
        if (redisAvailable.get()) {
            try {
                Object redisValue = redisTemplate.opsForValue().get(key);
                if (redisValue != null) {
                    localCache.put(key, redisValue);
                    return (T) redisValue;
                }
            } catch (RedisConnectionFailureException e) {
                redisAvailable.set(false);
                log.warn("Redis连接失败,切换到本地缓存降级模式");
                scheduleRedisRetry();
            }
        }
        
        // 3. 查数据库
        T dbValue = dbLoader.get();
        if (dbValue != null) {
            localCache.put(key, dbValue);
            if (redisAvailable.get()) {
                try {
                    redisTemplate.opsForValue().set(
                        key, dbValue, 
                        CacheUtil.getRandomExpire(), TimeUnit.SECONDS
                    );
                } catch (Exception ignored) {}
            }
        }
        return dbValue;
    }
    
    private void scheduleRedisRetry() {
        executor.schedule(() -> {
            try {
                redisTemplate.getConnectionFactory().getConnection().ping();
                redisAvailable.set(true);
                log.info("Redis恢复,切回正常模式");
            } catch (Exception e) {
                scheduleRedisRetry();
            }
        }, 5, TimeUnit.MINUTES);
    }
}

缓存击穿的热点Key保护方案

缓存击穿指单个热点Key过期瞬间,大量并发请求同时穿透到数据库。解决方案是互斥锁(Mutex Lock)和逻辑过期。

// 互斥锁方案——防止缓存重建并发
@Service
public class CacheBreakdownProtection {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    public <T> T getWithMutex(String key, Supplier<T> dbLoader, long expireSeconds) {
        String value = stringRedisTemplate.opsForValue().get(key);
        
        if (value != null) {
            return JSON.parseObject(value, new TypeReference<T>(){});
        }
        
        // 缓存未命中,获取互斥锁
        String lockKey = "lock:" + key;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            Boolean locked = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
            
            if (Boolean.TRUE.equals(locked)) {
                // 获取锁成功,查数据库并重建缓存
                T dbValue = dbLoader.get();
                stringRedisTemplate.opsForValue().set(
                    key, JSON.toJSONString(dbValue), 
                    expireSeconds, TimeUnit.SECONDS
                );
                return dbValue;
            } else {
                // 获取锁失败,短暂等待后重试
                Thread.sleep(50);
                return getWithMutex(key, dbLoader, expireSeconds);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("缓存重建被中断", e);
        } finally {
            // 释放锁(Lua脚本保证原子性)
            releaseLock(lockKey, lockValue);
        }
    }
    
    private void releaseLock(String lockKey, String lockValue) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] " +
            "then return redis.call('del', KEYS[1]) " +
            "else return 0 end";
        stringRedisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(lockKey),
            lockValue
        );
    }
}

逻辑过期方案不设置TTL,在value中存储逻辑过期时间。查询时判断逻辑过期,过期则异步重建缓存,当前请求返回旧数据。适用于可以容忍短暂数据不一致的场景:

// 逻辑过期方案
public <T> T getWithLogicalExpire(String key, Supplier<T> dbLoader) {
    String json = stringRedisTemplate.opsForValue().get(key);
    if (json == null) {
        return dbLoader.get();
    }
    
    CacheData<T> cacheData = JSON.parseObject(json, new TypeReference<>(){});
    
    if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
        return cacheData.getData();
    }
    
    // 已过期,尝试获取锁异步重建
    String lockKey = "lock:" + key;
    Boolean locked = stringRedisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    if (Boolean.TRUE.equals(locked)) {
        executor.submit(() -> {
            try {
                T newValue = dbLoader.get();
                CacheData<T> newData = new CacheData<>(
                    newValue, 
                    LocalDateTime.now().plusHours(1)
                );
                stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(newData));
            } finally {
                stringRedisTemplate.delete(lockKey);
            }
        });
    }
    
    return cacheData.getData();
}

缓存穿透的布隆过滤器防护

缓存穿透指查询不存在的数据,每次请求都穿透到数据库。布隆过滤器在缓存层之前做存在性判断:

// Redisson布隆过滤器实现
@Service
public class BloomFilterService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    private RBloomFilter<Long> productIdFilter;
    
    @PostConstruct
    public void init() {
        productIdFilter = redissonClient.getBloomFilter("product:id:filter");
        // 预计元素数量100万,误判率0.01%
        productIdFilter.tryInit(1_000_000L, 0.0001);
        
        List<Long> allIds = productMapper.selectAllIds();
        allIds.forEach(id -> productIdFilter.add(id));
    }
    
    public Product getProduct(Long id) {
        if (!productIdFilter.contains(id)) {
            return null;  // 肯定不存在,直接返回
        }
        
        String cacheKey = "product:" + id;
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        
        product = productMapper.selectById(id);
        if (product != null) {
            redisTemplate.opsForValue().set(
                cacheKey, product, 
                CacheUtil.getRandomExpire(), TimeUnit.SECONDS
            );
        } else {
            // 空值缓存,防止同一Key反复穿透
            redisTemplate.opsForValue().set(
                cacheKey, "NULL", 60, TimeUnit.SECONDS
            );
        }
        return product;
    }
}

熔断降级与Sentinel集成方案

当缓存层和数据库都承受不住压力时,需要通过熔断器快速失败保护系统不雪崩。Sentinel的慢调用比例熔断策略适合数据库保护场景:

// Sentinel熔断规则配置
@PostConstruct
public void initFlowRules() {
    List<DegradeRule> rules = new ArrayList<>();
    
    DegradeRule dbRule = new DegradeRule("dbQuery")
        .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
        .setCount(500)
        .setSlowRatioThreshold(0.6)
        .setMinRequestAmount(20)
        .setStatIntervalMs(10000)
        .setTimeWindow(10);
    rules.add(dbRule);
    
    DegradeRule errorRule = new DegradeRule("dbQuery")
        .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
        .setCount(0.5)
        .setMinRequestAmount(10)
        .setStatIntervalMs(5000)
        .setTimeWindow(15);
    rules.add(errorRule);
    
    DegradeRuleManager.loadRules(rules);
}

@SentinelResource(value = "dbQuery", 
    fallback = "fallbackQuery", 
    blockHandler = "blockHandler")
public Product queryProductFromDb(Long id) {
    return productMapper.selectById(id);
}

public Product fallbackQuery(Long id) {
    return Product.defaultProduct();
}

public Product blockHandler(Long id, BlockException ex) {
    log.warn("数据库查询被熔断, productId={}", id);
    return Product.defaultProduct();
}

生产环境的多级缓存架构配置建议:Caffeine本地缓存容量10000条,TTL 5分钟;Redis集群缓存容量按业务数据量配置,TTL 30-60分钟带随机偏移;数据库查询通过Sentinel熔断保护,慢调用阈值500ms,熔断窗口10秒。压测数据显示,该架构下单节点QPS从2000提升到15000,缓存命中率95%以上,数据库QPS降低90%。当Redis集群整体宕机时,Caffeine本地缓存扛住30%的读流量,剩余70%通过熔断降级快速返回,数据库不被压垮。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-xue-beng-yu-ji-chuan-fang-hu-shi-zhan-duo-ji/

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

相关推荐