缓存系统是现代互联网架构中不可或缺的一环,而Redis作为高性能的内存数据存储,广泛应用于各类缓存场景。在高并发系统中,缓存可能面临穿透、击穿和雪崩三大问题,这些问题若处理不当,会导致系统性能急剧下降甚至崩溃。

缓存穿透原理与应对

原理

缓存穿透指的是查询不存在的数据,请求绕过缓存直接访问数据库,导致数据库压力过大。常见场景有恶意攻击、错误查询或业务逻辑漏洞。

解决方案

缓存空对象

public Object get(String key) {
    // 先查缓存
    Object value = redis.get(key);
    if (value != null) {
        return value.equals("NULL") ? null : value;
    }
    
    // 缓存未命中,查数据库
    value = db.query(key);
    if (value == null) {
        // 缓存空对象,设置较短过期时间
        redis.setex(key, 60, "NULL");
    } else {
        redis.setex(key, 3600, value);
    }
    return value;
}

布隆过滤器

// 使用RedisBloom插件创建布隆过滤器
BF.RESERT myfilter 0.01 1000000

// 添加已存在的key到布隆过滤器
BF.ADD myfilter "user:1001"

// 查询前先检查布隆过滤器
public Object get(String key) {
    // 布隆过滤器检查
    if (!BF.EXISTS myfilter key) {
        return null;
    }
    
    // 后续缓存逻辑...
}

参数校验与业务限制

  • 对输入参数进行有效性校验
  • 限制单IP/用户的查询频率
  • 实现查询结果分页,避免全表查询

缓存击穿原理与应对

原理

缓存击穿指某个热点key突然失效,大量并发请求直接访问数据库,导致数据库压力激增。

解决方案

互斥锁/分布式锁

public Object getWithLock(String key) {
    // 先查缓存
    Object value = redis.get(key);
    if (value != null) {
        return value;
    }
    
    // 获取分布式锁
    String lockKey = "lock:" + key;
    try {
        boolean locked = redis.setnx(lockKey, "1", 10); // 10秒过期
        if (locked) {
            // 查询数据库
            value = db.query(key);
            if (value != null) {
                redis.setex(key, 3600, value);
            }
            return value;
        } else {
            // 未获取到锁,短暂休眠后重试
            Thread.sleep(50);
            return getWithLock(key);
        }
    } finally {
        redis.del(lockKey);
    }
}

永不过期策略

// 使用Redis的过期时间监听
public void listenExpiredEvents() {
    Jedis jedis = new Jedis("localhost");
    jedis.psubscribe(new JedisPubSub() {
        @Override
        public void onPMessage(String pattern, String channel, String message) {
            // 收到过期事件,异步更新缓存
            asyncRefreshCache(message);
        }
    }, "__keyevent@*__:expired");
}

热点数据预加载

  • 系统启动时预加载热点数据
  • 定时任务刷新热点数据
  • 基于访问频率预测热点数据

缓存雪崩原理与应对

原理

缓存雪崩指大量key在同一时间失效,导致大量请求直接访问数据库,造成数据库压力过大甚至宕机。

解决方案

缓存过期时间随机化

// 设置随机过期时间
public void setWithRandomExpire(String key, Object value, int baseExpire) {
    int expire = baseExpire + new Random().nextInt(300); // 增加0-5分钟随机时间
    redis.setex(key, expire, value);
}

多级缓存架构

客户端缓存 → 本地缓存 → Redis集群 → 数据库

降级与熔断策略

// 使用Hystrix实现熔断
public Object getWithCircuitBreaker(String key) {
    return HystrixCommandFactory.create()
        .run(() -> {
            // 正常缓存逻辑
            return redis.get(key);
        }, fallback -> {
            // 熔断降级逻辑
            if (fallback.isCircuitBreakerOpen()) {
                return "系统繁忙,请稍后重试";
            }
            return db.query(key);
        })
        .execute();
}

常见问题与注意事项

  1. 性能权衡:布隆过滤器有一定误判率,互斥锁可能增加响应时间,需根据业务场景选择合适方案。
  1. 实现陷阱:
  • 互斥锁过期时间应大于业务处理时间
  • 空对象缓存时间不宜过长
  • 随机化过期时间范围需合理设置
  1. 监控与告警:
  • 监控缓存命中率
  • 监控数据库负载
  • 设置合理的告警阈值

总结

缓存穿透、击穿和雪崩是Redis缓存系统中的常见问题。针对穿透问题,可采用缓存空对象、布隆过滤器或参数校验;针对击穿问题,可使用互斥锁、永不过期策略或热点数据预加载;针对雪崩问题,可采用过期时间随机化、多级缓存、降级熔断或缓存预热策略。实际应用中,应根据业务特点、数据访问模式和系统负载,选择合适的解决方案组合,并做好监控和应急预案,以确保系统在高并发场景下的稳定运行。

主楼