Redis缓存策略设计:穿透、击穿与雪崩的工程化解决方案

Redis缓存策略设计中,缓存穿透、缓存击穿和缓存雪崩是三类经典问题。这三个问题如果不做工程化处理,在高并发场景下会导致数据库连接池耗尽、响应超时甚至服务雪崩。本文通过布隆过滤器、互斥锁、随机TTL和多级缓存四种方案,给出完整的工程化解决方案和Java代码实现。

缓存穿透问题与布隆过滤器

缓存穿透指大量请求查询不存在的数据,请求绕过缓存直接打到数据库。常见原因是恶意攻击(如用不存在的ID发请求)或业务Bug。数据库频繁查询不存在的记录,会导致连接池耗尽和响应超时。

解决方案分两层:布隆过滤器拦截和空值缓存。布隆过滤器通过位数组和哈希函数判断元素是否存在。判断不存在则一定不存在,判断存在可能有误判。将数据库中存在的Key预热到布隆过滤器中,请求先经过布隆过滤器校验:

// Redisson布隆过滤器实现
@Configuration
public class BloomFilterConfig {

    @Bean
    public RBloomFilter<Long> userBloomFilter(RedissonClient redisson) {
        RBloomFilter<Long> filter = redisson.getBloomFilter("user:bloom");
        // 初始化:预计元素100万,误判率0.01%
        filter.tryInit(1_000_000L, 0.0001);
        return filter;
    }
}

// 查询逻辑
@Service
public class UserService {

    @Autowired
    private RBloomFilter<Long> userBloomFilter;

    @Autowired
    private RedisTemplate<String, User> redisTemplate;

    @Autowired
    private UserMapper userMapper;

    public User getUserById(Long id) {
        // 第一层:布隆过滤器校验
        if (!userBloomFilter.contains(id)) {
            return null;  // 一定不存在,直接返回
        }

        // 第二层:查缓存
        String key = "user:" + id;
        User user = redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }

        // 第三层:查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        } else {
            // 空值缓存,短TTL
            redisTemplate.opsForValue().set(key, new User(), 5, TimeUnit.MINUTES);
        }
        return user;
    }
}

空值缓存设置较短的TTL(5分钟),防止数据库后续新增了该记录但缓存中一直是空值。布隆过滤器的误判率设置越低,位数组占用内存越大。100万数据量下,0.01%误判率约需要1.2MB内存。

缓存击穿与互斥锁方案

缓存击穿指某个热点Key在缓存中过期的瞬间,大量并发请求同时查询该Key,全部穿透到数据库。与穿透不同,击穿的数据是真实存在的,只是缓存恰好失效。

互斥锁(Mutex Lock)是解决缓存击穿的经典方案。同一时间只允许一个线程查询数据库并重建缓存,其他线程等待或重试:

public User getUserWithMutex(Long id) {
    String key = "user:" + id;
    User user = redisTemplate.opsForValue().get(key);

    if (user != null) {
        return user;
    }

    // 缓存未命中,尝试获取互斥锁
    String lockKey = "lock:user:" + id;
    try {
        // 尝试加锁,等待3秒,锁自动释放10秒
        boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

        if (locked) {
            try {
                // 双重检查:获取锁后再次查缓存
                user = redisTemplate.opsForValue().get(key);
                if (user != null) {
                    return user;
                }

                // 查数据库
                user = userMapper.selectById(id);
                if (user != null) {
                    // 随机TTL防止雪崩
                    int ttl = 1800 + ThreadLocalRandom.current().nextInt(600);
                    redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
                }
                return user;
            } finally {
                // 释放锁
                redisTemplate.delete(lockKey);
            }
        } else {
            // 未获取到锁,短暂等待后重试
            Thread.sleep(50);
            return getUserWithMutex(id);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取用户信息被中断", e);
    }
}

双重检查(Double Check)是关键设计:获取锁后再次查询缓存,避免前一个持锁线程已经重建缓存但当前线程仍执行数据库查询。锁的自动过期时间(10秒)需要大于数据库查询时间,防止锁提前释放导致多个线程同时查库。

缓存雪崩与随机TTL策略

缓存雪崩指大量Key在同一时间过期,或Redis节点宕机,导致大量请求同时打到数据库。与击穿的区别在于雪崩是大量Key同时失效,而非单个热点Key。

核心预防措施是给TTL添加随机偏移量,避免大量Key同时过期:

public void cacheUser(User user) {
    String key = "user:" + user.getId();
    // 基础TTL 30分钟 + 随机0-10分钟
    int baseTTL = 1800;
    int randomTTL = ThreadLocalRandom.current().nextInt(600);
    redisTemplate.opsForValue().set(key, user, baseTTL + randomTTL, TimeUnit.SECONDS);
}

// 批量缓存时同样需要随机化
public void batchCacheUsers(List<User> users) {
    users.forEach(user -> {
        String key = "user:" + user.getId();
        int ttl = 1800 + ThreadLocalRandom.current().nextInt(600);
        redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
    });
}

Redis节点宕机导致的雪崩需要从架构层面解决。Redis Cluster提供自动failover能力,但当主节点切换期间,大量请求会失败。兜底方案是本地缓存(Caffeine)作为二级缓存:

@Configuration
public class MultiLevelCacheConfig {

    @Bean
    public Cache<String, User> localCache() {
        return Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()
            .build();
    }
}

@Service
public class MultiLevelCacheService {

    @Autowired
    private Cache<String, User> localCache;

    @Autowired
    private RedisTemplate<String, User> redisTemplate;

    public User getUser(Long id) {
        String key = "user:" + id;

        // L1:本地缓存
        User user = localCache.getIfPresent(key);
        if (user != null) {
            return user;
        }

        // L2:Redis缓存
        user = redisTemplate.opsForValue().get(key);
        if (user != null) {
            localCache.put(key, user);
            return user;
        }

        // L3:数据库
        user = userMapper.selectById(id);
        if (user != null) {
            int ttl = 1800 + ThreadLocalRandom.current().nextInt(600);
            redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
            localCache.put(key, user);
        }
        return user;
    }
}

本地缓存的TTL应短于Redis缓存,确保数据一致性。本地缓存命中率可通过Caffeine的recordStats()监控,如果命中率低于20%,说明缓存Key分散度过高,需要调整maximumSize或预热策略。

缓存监控与告警配置

Redis缓存策略的效果需要通过监控数据验证。关键监控指标包括缓存命中率、内存使用率、连接数和慢查询。生产环境建议使用Redis Exporter + Prometheus + Grafana实现自动化监控告警:

# Prometheus告警规则
groups:
  - name: redis_cache
    rules:
      - alert: RedisLowHitRate
        expr: |
          redis_keyspace_hits_total{instance="redis:9121"}
          / (redis_keyspace_hits_total{instance="redis:9121"}
             + redis_keyspace_misses_total{instance="redis:9121"})
          * 100 < 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis缓存命中率低于80%"

      - alert: RedisHighMemoryUsage
        expr: redis_memory_used_bytes{instance="redis:9121"}
              / redis_memory_max_bytes{instance="redis:9121"} * 100 > 85
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Redis内存使用率超过85%"

命中率告警阈值为80%,持续5分钟触发。内存使用率告警阈值85%,持续2分钟触发critical级别告警。收到告警后,需要排查是否存在缓存Key设计不合理(如缓存粒度过细导致命中率低)、或者是否需要扩容Redis实例。数据库高可用架构设计中,Redis缓存作为数据库的前置防线,其稳定性直接影响整个系统的可用性。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ce-lyue-she-ji-chuan-tou-ji-chuan-yu-xue/

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

相关推荐