48-缓存设计详解

2 阅读14分钟

缓存设计详解

一、知识概述

缓存是提升系统性能的重要手段,但不合理的缓存设计可能导致缓存穿透、缓存雪崩、缓存击穿等问题,甚至引发系统故障。本文将详细介绍这些问题的原理和解决方案,帮助设计健壮的缓存系统。

二、缓存穿透

2.1 问题介绍

缓存穿透是指查询一个根本不存在的数据,由于缓存中没有数据,每次都会查询数据库,导致缓存失去了保护数据库的作用。

场景举例

  • 恶意请求查询不存在的用户 ID
  • 查询不存在的商品信息
  • 查询不存在的订单号

危害

  • 数据库压力骤增
  • 可能导致数据库宕机
  • 浪费系统资源

2.2 解决方案

方案一:缓存空值

当查询数据库没有结果时,将空值写入缓存,并设置较短的过期时间。

// 缓存空值解决方案
public class CacheNullValue {
    private Jedis jedis;
    
    public String getData(String key) {
        // 1. 查询缓存
        String value = jedis.get(key);
        
        // 2. 缓存存在,直接返回
        if (value != null) {
            // 判断是否为空值标记
            if ("NULL".equals(value)) {
                return null;
            }
            return value;
        }
        
        // 3. 查询数据库
        value = queryFromDB(key);
        
        // 4. 写入缓存
        if (value != null) {
            jedis.setex(key, 3600, value);
        } else {
            // 缓存空值,过期时间较短
            jedis.setex(key, 60, "NULL");
        }
        
        return value;
    }
    
    private String queryFromDB(String key) {
        // 模拟数据库查询
        return null;
    }
}

优点:实现简单,有效防止穿透 缺点

  • 占用缓存内存
  • 可能缓存大量无效 key
  • 数据一致性窗口期
方案二:布隆过滤器

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

// 布隆过滤器实现(简化版)
public class BloomFilter {
    private BitSet bitSet;
    private int size;
    private int[] hashSeeds;
    
    public BloomFilter(int size, int hashCount) {
        this.size = size;
        this.bitSet = new BitSet(size);
        this.hashSeeds = new int[hashCount];
        Random random = new Random();
        for (int i = 0; i < hashCount; i++) {
            hashSeeds[i] = random.nextInt();
        }
    }
    
    // 添加元素
    public void add(String value) {
        for (int seed : hashSeeds) {
            int hash = hash(value, seed);
            bitSet.set(hash);
        }
    }
    
    // 判断元素是否存在
    public boolean mightContain(String value) {
        for (int seed : hashSeeds) {
            int hash = hash(value, seed);
            if (!bitSet.get(hash)) {
                return false;
            }
        }
        return true;
    }
    
    private int hash(String value, int seed) {
        int hash = 0;
        for (char c : value.toCharArray()) {
            hash = hash * seed + c;
        }
        return Math.abs(hash) % size;
    }
}

// 使用布隆过滤器防止穿透
public class CacheWithBloomFilter {
    private Jedis jedis;
    private BloomFilter bloomFilter;
    
    public String getData(String key) {
        // 1. 先检查布隆过滤器
        if (!bloomFilter.mightContain(key)) {
            // 确定不存在,直接返回
            return null;
        }
        
        // 2. 查询缓存
        String value = jedis.get(key);
        if (value != null) {
            return "NULL".equals(value) ? null : value;
        }
        
        // 3. 查询数据库
        value = queryFromDB(key);
        
        // 4. 写入缓存
        if (value != null) {
            jedis.setex(key, 3600, value);
        } else {
            // 可能是误判,仍然缓存空值
            jedis.setex(key, 60, "NULL");
        }
        
        return value;
    }
    
    // 初始化布隆过滤器
    public void initBloomFilter() {
        // 从数据库加载所有有效 key
        List<String> allKeys = loadAllKeysFromDB();
        for (String key : allKeys) {
            bloomFilter.add(key);
        }
    }
    
    private List<String> loadAllKeysFromDB() {
        // 实现从数据库加载
        return new ArrayList<>();
    }
    
    private String queryFromDB(String key) {
        return null;
    }
}

使用 Redis 实现 Bloom Filter

// Redis 布隆过滤器
public class RedisBloomFilter {
    private Jedis jedis;
    private String key;
    private int size;
    private int hashCount;
    
    public RedisBloomFilter(Jedis jedis, String key, int size, int hashCount) {
        this.jedis = jedis;
        this.key = key;
        this.size = size;
        this.hashCount = hashCount;
    }
    
    // 添加元素
    public void add(String value) {
        for (int i = 0; i < hashCount; i++) {
            int hash = hash(value, i);
            jedis.setbit(key, hash, true);
        }
    }
    
    // 判断是否存在
    public boolean mightContain(String value) {
        for (int i = 0; i < hashCount; i++) {
            int hash = hash(value, i);
            if (!jedis.getbit(key, hash)) {
                return false;
            }
        }
        return true;
    }
    
    // 使用 MurmurHash(更好哈希算法)
    private int hash(String value, int seed) {
        // 简化实现,生产环境建议使用 Guava 的 BloomFilter
        return Math.abs((value.hashCode() * (seed + 1)) % size);
    }
}

// 使用 Guava BloomFilter
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class GuavaBloomFilterDemo {
    public static void main(String[] args) {
        // 创建布隆过滤器
        BloomFilter<String> filter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            1000000,  // 预期元素数量
            0.01      // 误判率
        );
        
        // 添加元素
        filter.put("user:1001");
        filter.put("user:1002");
        
        // 判断是否存在
        System.out.println(filter.mightContain("user:1001"));  // true
        System.out.println(filter.mightContain("user:9999"));  // false(大概率)
    }
}

优点

  • 空间效率高
  • 查询速度快
  • 不存在误判不存在的情况

缺点

  • 存在误判(判断存在但实际不存在)
  • 不支持删除
  • 需要预加载数据
方案三:参数校验

在请求到达缓存之前,先进行参数合法性校验。

// 参数校验拦截
public class RequestValidator {
    
    public boolean isValidRequest(String key) {
        // 1. 基础校验
        if (key == null || key.isEmpty()) {
            return false;
        }
        
        // 2. 格式校验
        if (!key.matches("^user:\\d+$")) {
            return false;
        }
        
        // 3. 范围校验
        String idStr = key.substring(5);
        try {
            long id = Long.parseLong(idStr);
            if (id < 1 || id > 1000000000L) {
                return false;
            }
        } catch (NumberFormatException e) {
            return false;
        }
        
        return true;
    }
}

// 在 Controller 层使用
@RestController
public class UserController {
    
    @Autowired
    private RequestValidator validator;
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable String id) {
        String key = "user:" + id;
        
        // 参数校验
        if (!validator.isValidRequest(key)) {
            throw new IllegalArgumentException("Invalid user id");
        }
        
        return userService.getUser(key);
    }
}

2.3 综合解决方案

// 完整的防穿透方案
public class AntiPenetrationCache {
    private Jedis jedis;
    private BloomFilter<String> bloomFilter;
    private static final String NULL_VALUE = "##NULL##";
    
    public String getData(String key) {
        // 1. 参数校验
        if (!isValidKey(key)) {
            return null;
        }
        
        // 2. 布隆过滤器检查
        if (!bloomFilter.mightContain(key)) {
            return null;
        }
        
        // 3. 缓存查询
        String value = jedis.get(key);
        if (value != null) {
            return NULL_VALUE.equals(value) ? null : value;
        }
        
        // 4. 数据库查询
        value = queryFromDB(key);
        
        // 5. 写入缓存
        if (value != null) {
            jedis.setex(key, getRandomExpireTime(), value);
        } else {
            // 缓存空值,短过期时间
            jedis.setex(key, 60, NULL_VALUE);
        }
        
        return value;
    }
    
    // 随机过期时间,防止同时过期
    private int getRandomExpireTime() {
        return 3600 + new Random().nextInt(600);
    }
    
    private boolean isValidKey(String key) {
        return key != null && key.matches("^\\w+:\\d+$");
    }
    
    private String queryFromDB(String key) {
        return null;
    }
}

三、缓存雪崩

3.1 问题介绍

缓存雪崩是指大量缓存同时失效Redis 服务宕机,导致所有请求直接打到数据库,造成数据库压力骤增甚至宕机。

场景举例

  • 大量 key 设置了相同的过期时间
  • Redis 服务重启或宕机
  • 缓存预热时间集中

危害

  • 数据库瞬间压力巨大
  • 系统响应变慢甚至不可用
  • 连锁反应导致整个系统崩溃

3.2 解决方案

方案一:过期时间加随机值

在设置缓存过期时间时,添加随机值,避免同时过期。

// 随机过期时间
public class RandomExpireCache {
    private Jedis jedis;
    
    // 基础过期时间(秒)
    private int baseExpireTime = 3600;
    
    // 随机范围(秒)
    private int randomRange = 600;
    
    public void set(String key, String value) {
        int expireTime = baseExpireTime + new Random().nextInt(randomRange);
        jedis.setex(key, expireTime, value);
    }
    
    public void batchSet(Map<String, String> data) {
        for (Map.Entry<String, String> entry : data.entrySet()) {
            set(entry.getKey(), entry.getValue());
        }
    }
}
方案二:多级缓存

使用多级缓存,即使一级缓存失效,二级缓存仍可提供数据。

// 多级缓存实现
public class MultiLevelCache {
    // 一级缓存:本地缓存(Caffeine)
    private Cache<String, String> localCache;
    
    // 二级缓存:Redis
    private Jedis jedis;
    
    public MultiLevelCache() {
        // 本地缓存配置
        localCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(300, TimeUnit.SECONDS)  // 5分钟过期
            .build();
    }
    
    public String get(String key) {
        // 1. 查询一级缓存(本地)
        String value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 查询二级缓存(Redis)
        value = jedis.get(key);
        if (value != null) {
            // 回填一级缓存
            localCache.put(key, value);
            return value;
        }
        
        // 3. 查询数据库
        value = queryFromDB(key);
        if (value != null) {
            // 写入多级缓存
            jedis.setex(key, 3600, value);
            localCache.put(key, value);
        }
        
        return value;
    }
    
    // 使用分布式锁防止缓存重建时的并发问题
    public String getWithLock(String key) {
        // 1. 查询本地缓存
        String value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 查询 Redis
        value = jedis.get(key);
        if (value != null) {
            localCache.put(key, value);
            return value;
        }
        
        // 3. 获取分布式锁,防止重复查询数据库
        String lockKey = "lock:" + key;
        try {
            if (tryLock(lockKey, 10)) {
                // 双重检查
                value = jedis.get(key);
                if (value != null) {
                    localCache.put(key, value);
                    return value;
                }
                
                // 查询数据库
                value = queryFromDB(key);
                
                // 写入缓存
                if (value != null) {
                    jedis.setex(key, 3600, value);
                    localCache.put(key, value);
                }
            } else {
                // 获取锁失败,等待后重试
                Thread.sleep(50);
                return getWithLock(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            unlock(lockKey);
        }
        
        return value;
    }
    
    private boolean tryLock(String key, int expireSeconds) {
        return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
    }
    
    private void unlock(String key) {
        jedis.del(key);
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}
方案三:熔断降级

当缓存失效导致数据库压力过大时,触发熔断,保护系统。

// 熔断降级实现(使用 Resilience4j)
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;

public class CircuitBreakerCache {
    private Jedis jedis;
    private CircuitBreaker circuitBreaker;
    
    public CircuitBreakerCache() {
        // 熔断器配置
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)  // 失败率阈值 50%
            .waitDurationInOpenState(Duration.ofSeconds(30))  // 开启状态等待时间
            .slidingWindowSize(100)  // 滑动窗口大小
            .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
            .build();
        
        circuitBreaker = CircuitBreaker.of("cacheBreaker", config);
    }
    
    public String get(String key) {
        // 使用熔断器包装查询逻辑
        try {
            return CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
                String value = jedis.get(key);
                if (value == null) {
                    // 查询数据库
                    value = queryFromDB(key);
                    if (value != null) {
                        jedis.setex(key, 3600, value);
                    }
                }
                return value;
            }).get();
        } catch (Exception e) {
            // 熔断后返回降级数据
            return getFallbackValue(key);
        }
    }
    
    private String getFallbackValue(String key) {
        // 返回降级数据或默认值
        return "default_value";
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}
方案四:缓存预热

在系统启动或低峰期,提前加载热点数据到缓存。

// 缓存预热实现
public class CacheWarmUp {
    private Jedis jedis;
    
    // 应用启动时执行预热
    @PostConstruct
    public void warmUp() {
        // 1. 加载热点数据
        List<String> hotKeys = loadHotKeys();
        
        // 2. 批量写入缓存(使用 Pipeline 提高效率)
        Pipeline pipeline = jedis.pipelined();
        for (String key : hotKeys) {
            String value = queryFromDB(key);
            if (value != null) {
                // 随机过期时间
                int expire = 3600 + new Random().nextInt(600);
                pipeline.setex(key, expire, value);
            }
        }
        pipeline.sync();
        
        System.out.println("Cache warm-up completed: " + hotKeys.size() + " keys");
    }
    
    // 定时预热(更新热点数据)
    @Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨3点
    public void scheduledWarmUp() {
        warmUp();
    }
    
    private List<String> loadHotKeys() {
        // 从配置或日志中加载热点 key
        // 例如:访问频率前 1000 的 key
        return Arrays.asList("hot1", "hot2", "hot3");
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}

3.3 综合解决方案

// 完整的防雪崩方案
public class AntiAvalancheCache {
    private Jedis jedis;
    private Cache<String, String> localCache;
    private CircuitBreaker circuitBreaker;
    
    public AntiAvalancheCache() {
        // 本地缓存
        localCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(300, TimeUnit.SECONDS)
            .build();
        
        // 熔断器
        circuitBreaker = CircuitBreaker.of("cacheBreaker",
            CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(30))
                .slidingWindowSize(100)
                .build()
        );
    }
    
    public String get(String key) {
        // 1. 本地缓存
        String value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 熔断保护下的查询
        try {
            value = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
                // Redis 查询
                String v = jedis.get(key);
                if (v == null) {
                    // 数据库查询(带互斥锁)
                    v = queryFromDBWithLock(key);
                }
                return v;
            }).get();
            
            // 3. 回填本地缓存
            if (value != null) {
                localCache.put(key, value);
            }
            
            return value;
            
        } catch (Exception e) {
            // 4. 降级处理
            return getFallback(key);
        }
    }
    
    private String queryFromDBWithLock(String key) {
        String lockKey = "lock:" + key;
        try {
            // 尝试获取锁
            if (tryLock(lockKey, 10)) {
                // 双重检查
                String value = jedis.get(key);
                if (value != null) {
                    return value;
                }
                
                // 查询数据库
                value = queryFromDB(key);
                
                // 写入缓存(随机过期时间)
                if (value != null) {
                    int expire = 3600 + new Random().nextInt(600);
                    jedis.setex(key, expire, value);
                }
                
                return value;
            } else {
                // 获取锁失败,等待重试
                Thread.sleep(50);
                String value = jedis.get(key);
                return value != null ? value : queryFromDBWithLock(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            unlock(lockKey);
        }
    }
    
    private String getFallback(String key) {
        // 降级策略:返回默认值或走备份数据源
        return "default_value";
    }
    
    private boolean tryLock(String key, int expireSeconds) {
        return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
    }
    
    private void unlock(String key) {
        jedis.del(key);
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}

四、缓存击穿

4.1 问题介绍

缓存击穿是指某个热点 key 过期被删除,此时大量并发请求同时访问该 key,都发现缓存不存在,同时去查询数据库,导致数据库压力骤增。

与雪崩的区别

  • 雪崩:大量 key 同时失效
  • 击穿:单个热点 key 失效

场景举例

  • 热门商品信息缓存过期
  • 新闻头条缓存过期
  • 限时秒杀活动数据

4.2 解决方案

方案一:互斥锁

使用分布式锁,只让一个请求查询数据库,其他请求等待。

// 互斥锁解决方案
public class MutexLockCache {
    private Jedis jedis;
    
    public String get(String key) {
        // 1. 查询缓存
        String value = jedis.get(key);
        if (value != null) {
            return value;
        }
        
        // 2. 尝试获取分布式锁
        String lockKey = "lock:" + key;
        try {
            if (tryLock(lockKey, 10)) {
                // 3. 获取锁成功,查询数据库
                // 双重检查,防止其他线程已经更新缓存
                value = jedis.get(key);
                if (value != null) {
                    return value;
                }
                
                // 查询数据库
                value = queryFromDB(key);
                
                // 写入缓存
                if (value != null) {
                    jedis.setex(key, 3600, value);
                }
                
                return value;
            } else {
                // 4. 获取锁失败,等待后重试
                return waitAndRetry(key);
            }
        } finally {
            unlock(lockKey);
        }
    }
    
    private String waitAndRetry(String key) {
        // 等待 50ms 后重试
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return get(key);
    }
    
    private boolean tryLock(String key, int expireSeconds) {
        return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
    }
    
    private void unlock(String key) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        jedis.eval(script, 1, key, "1");
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}
方案二:热点数据永不过期

对于确定的热点数据,可以设置为永不过期,通过后台任务定期更新。

// 热点数据永不过期方案
public class HotDataCache {
    private Jedis jedis;
    
    // 物理过期时间(实际存储)
    private static final int PHYSICAL_EXPIRE = 86400;  // 24小时
    
    // 逻辑过期时间(数据中的字段)
    private static final int LOGICAL_EXPIRE = 3600;  // 1小时
    
    // 存储数据(包含逻辑过期时间)
    public void set(String key, String value) {
        CacheData data = new CacheData(value, System.currentTimeMillis() + LOGICAL_EXPIRE * 1000);
        jedis.setex(key, PHYSICAL_EXPIRE, JSON.toJSONString(data));
    }
    
    // 获取数据
    public String get(String key) {
        String json = jedis.get(key);
        if (json == null) {
            // 真正过期,查询数据库
            String value = queryFromDB(key);
            if (value != null) {
                set(key, value);
            }
            return value;
        }
        
        CacheData data = JSON.parseObject(json, CacheData.class);
        
        // 检查逻辑过期
        if (data.expireTime < System.currentTimeMillis()) {
            // 逻辑过期,异步更新
            asyncRefresh(key);
        }
        
        return data.value;
    }
    
    // 异步刷新缓存
    private void asyncRefresh(String key) {
        CompletableFuture.runAsync(() -> {
            String value = queryFromDB(key);
            if (value != null) {
                set(key, value);
            }
        });
    }
    
    static class CacheData {
        String value;
        long expireTime;
        
        CacheData(String value, long expireTime) {
            this.value = value;
            this.expireTime = expireTime;
        }
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}
方案三:提前续期

在缓存即将过期前,提前续期,保证热点数据不会过期。

// 提前续期方案
public class RenewCache {
    private Jedis jedis;
    
    // 过期时间
    private static final int EXPIRE_TIME = 3600;  // 1小时
    
    // 续期阈值(剩余时间小于此值则续期)
    private static final int RENEW_THRESHOLD = 600;  // 10分钟
    
    public String get(String key) {
        String value = jedis.get(key);
        
        if (value != null) {
            // 检查剩余时间
            long ttl = jedis.ttl(key);
            if (ttl > 0 && ttl < RENEW_THRESHOLD) {
                // 异步续期
                asyncRenew(key, value);
            }
            return value;
        }
        
        // 查询数据库
        value = queryFromDB(key);
        if (value != null) {
            jedis.setex(key, EXPIRE_TIME, value);
        }
        
        return value;
    }
    
    private void asyncRenew(String key, String value) {
        CompletableFuture.runAsync(() -> {
            // 重新设置过期时间
            jedis.setex(key, EXPIRE_TIME, value);
        });
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
}

五、综合缓存架构

5.1 完整缓存方案

// 完整的缓存架构实现
public class CompleteCacheSystem {
    // 一级缓存:本地缓存
    private Cache<String, CacheData> localCache;
    
    // 二级缓存:Redis
    private Jedis jedis;
    
    // 布隆过滤器
    private BloomFilter<String> bloomFilter;
    
    // 熔断器
    private CircuitBreaker circuitBreaker;
    
    public CompleteCacheSystem() {
        // 初始化本地缓存
        localCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(300, TimeUnit.SECONDS)
            .build();
        
        // 初始化布隆过滤器
        bloomFilter = BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()),
            1000000, 0.01
        );
        loadBloomFilter();
        
        // 初始化熔断器
        circuitBreaker = CircuitBreaker.of("cacheBreaker",
            CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(30))
                .slidingWindowSize(100)
                .build()
        );
    }
    
    public String get(String key) {
        // 1. 参数校验
        if (!isValidKey(key)) {
            return null;
        }
        
        // 2. 布隆过滤器检查(防穿透)
        if (!bloomFilter.mightContain(key)) {
            return null;
        }
        
        // 3. 本地缓存查询
        CacheData localData = localCache.getIfPresent(key);
        if (localData != null) {
            // 检查逻辑过期
            if (localData.expireTime > System.currentTimeMillis()) {
                return localData.value;
            }
            // 逻辑过期,异步更新
            asyncRefresh(key);
            return localData.value;
        }
        
        // 4. Redis 查询(带熔断保护)
        try {
            String value = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
                return getFromRedis(key);
            }).get();
            
            // 5. 回填本地缓存
            if (value != null) {
                localCache.put(key, new CacheData(value, 
                    System.currentTimeMillis() + 300 * 1000));
            }
            
            return value;
            
        } catch (Exception e) {
            // 6. 降级处理
            return getFallback(key);
        }
    }
    
    private String getFromRedis(String key) {
        String value = jedis.get(key);
        
        if (value != null) {
            // 检查是否需要续期(防击穿)
            long ttl = jedis.ttl(key);
            if (ttl > 0 && ttl < 600) {
                asyncRenew(key, value);
            }
            return value;
        }
        
        // 使用互斥锁查询数据库(防击穿)
        return queryFromDBWithLock(key);
    }
    
    private String queryFromDBWithLock(String key) {
        String lockKey = "lock:" + key;
        try {
            if (tryLock(lockKey, 10)) {
                // 双重检查
                String value = jedis.get(key);
                if (value != null) {
                    return value;
                }
                
                // 查询数据库
                value = queryFromDB(key);
                
                // 写入缓存(随机过期时间,防雪崩)
                if (value != null) {
                    int expire = 3600 + new Random().nextInt(600);
                    jedis.setex(key, expire, value);
                    // 更新布隆过滤器
                    bloomFilter.put(key);
                } else {
                    // 缓存空值(防穿透)
                    jedis.setex(key, 60, "NULL");
                }
                
                return value;
            } else {
                // 等待重试
                Thread.sleep(50);
                return jedis.get(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            unlock(lockKey);
        }
    }
    
    private void asyncRefresh(String key) {
        CompletableFuture.runAsync(() -> {
            String value = queryFromDB(key);
            if (value != null) {
                int expire = 3600 + new Random().nextInt(600);
                jedis.setex(key, expire, value);
                localCache.put(key, new CacheData(value,
                    System.currentTimeMillis() + 300 * 1000));
            }
        });
    }
    
    private void asyncRenew(String key, String value) {
        CompletableFuture.runAsync(() -> {
            jedis.setex(key, 3600, value);
        });
    }
    
    private void loadBloomFilter() {
        // 从数据库加载所有 key 到布隆过滤器
        List<String> keys = loadAllKeys();
        for (String key : keys) {
            bloomFilter.put(key);
        }
    }
    
    private String getFallback(String key) {
        // 返回降级数据
        return "default_value";
    }
    
    private boolean isValidKey(String key) {
        return key != null && key.matches("^\\w+:\\d+$");
    }
    
    private boolean tryLock(String key, int expireSeconds) {
        return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
    }
    
    private void unlock(String key) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        jedis.eval(script, 1, key, "1");
    }
    
    private List<String> loadAllKeys() {
        return new ArrayList<>();
    }
    
    private String queryFromDB(String key) {
        return "data:" + key;
    }
    
    static class CacheData {
        String value;
        long expireTime;
        
        CacheData(String value, long expireTime) {
            this.value = value;
            this.expireTime = expireTime;
        }
    }
}

六、总结与最佳实践

6.1 问题对比总结

问题原因危害解决方案
缓存穿透查询不存在的数据数据库压力增大布隆过滤器、缓存空值、参数校验
缓存雪崩大量缓存同时失效数据库压力骤增随机过期时间、多级缓存、熔断降级
缓存击穿热点 key 过期数据库压力大互斥锁、永不过期、提前续期

6.2 最佳实践

  1. 缓存穿透防护

    • 使用布隆过滤器预过滤
    • 缓存空值(短过期时间)
    • 严格的参数校验
  2. 缓存雪崩防护

    • 过期时间加随机值
    • 使用多级缓存
    • 实施熔断降级
    • 做好缓存预热
  3. 缓存击穿防护

    • 使用分布式互斥锁
    • 热点数据永不过期
    • 提前续期机制
  4. 监控告警

    • 监控缓存命中率
    • 监控数据库查询量
    • 设置合理的告警阈值
  5. 容灾设计

    • Redis 高可用部署
    • 数据库限流
    • 服务降级预案

七、思考与练习

思考题

  1. 基础题:缓存穿透、缓存雪崩、缓存击穿三者有什么区别?请分别描述它们的典型场景。

  2. 进阶题:布隆过滤器为什么可能产生误判?在实际项目中如何降低误判率?误判会带来什么影响?

  3. 实战题:设计一个电商商品详情页的缓存方案,需要同时防护穿透、雪崩、击穿三种问题,请画出架构图并说明关键设计点。

编程练习

练习:实现一个带自动预热和智能续期的热点数据缓存管理器,要求:

  • 支持设置热点数据标识
  • 热点数据即将过期前自动异步续期
  • 非热点数据过期后正常重新加载
  • 提供统计数据接口(命中率、续期次数等)

提示:使用 Redis 的 TTL 命令获取剩余时间,结合线程池实现异步续期。

章节关联

  • 前置章节:《Redis集群详解》- 掌握 Redis 高可用方案
  • 后续章节:《缓存一致性详解》- 深入学习缓存与数据库的一致性问题
  • 扩展阅读:《大型网站技术架构》缓存章节

📝 下一章预告

下一章将深入探讨缓存一致性问题,包括缓存更新策略、强一致性方案、最终一致性保证等核心话题,解决分布式系统中最棘手的数据同步难题。


本章完