8.4 缓存设计模式

面试重要程度:⭐⭐⭐⭐⭐

常见提问方式: 缓存穿透击穿雪崩、分布式锁、热点数据处理

预计阅读时间:45分钟

🎯 缓存穿透、击穿、雪崩解决方案

缓存穿透

面试官: "什么是缓存穿透?如何解决?"

问题定义:

/**
 * 缓存穿透:查询不存在的数据
 * 现象:缓存和数据库都没有数据,但请求持续打到数据库
 * 危害:数据库压力过大,可能导致系统崩溃
 */
public class CachePenetration {
    
    public User getUserById(Long userId) {
        // 1. 查询缓存
        User user = redisTemplate.opsForValue().get("user:" + userId);
        if (user != null) {
            return user;
        }
        
        // 2. 缓存未命中,查询数据库
        user = userMapper.selectById(userId);
        if (user != null) {
            // 3. 存入缓存
            redisTemplate.opsForValue().set("user:" + userId, user, 30, TimeUnit.MINUTES);
        }
        // 4. 问题:恶意请求不存在的userId会一直打到数据库
        return user;
    }
}

解决方案1:缓存空值

@Service
public class CacheNullService {
    
    public User getUserById(Long userId) {
        String key = "user:" + userId;
        
        // 1. 查询缓存
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
            // 如果是空值标记,返回null
            if ("NULL".equals(cached)) {
                return null;
            }
            return (User) cached;
        }
        
        // 2. 查询数据库
        User user = userMapper.selectById(userId);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        } else {
            // 缓存空值,设置较短过期时间
            redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
        }
        
        return user;
    }
}

解决方案2:布隆过滤器

@Configuration
public class BloomFilterConfig {
    
    @Bean
    public BloomFilter<Long> userBloomFilter() {
        // 预期插入100万个元素,误判率0.01%
        return BloomFilter.create(Funnels.longFunnel(), 1000000, 0.0001);
    }
}

@Service
public class BloomFilterService {
    
    @Autowired
    private BloomFilter<Long> userBloomFilter;
    
    public User getUserById(Long userId) {
        // 1. 布隆过滤器判断
        if (!userBloomFilter.mightContain(userId)) {
            // 一定不存在,直接返回
            return null;
        }
        
        // 2. 可能存在,继续查询缓存和数据库
        String key = "user:" + userId;
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return "NULL".equals(cached) ? null : (User) cached;
        }
        
        User user = userMapper.selectById(userId);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        } else {
            redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
        }
        
        return user;
    }
}

缓存击穿

面试官: "缓存击穿和缓存穿透有什么区别?如何解决?"

解决方案1:互斥锁

@Service
public class MutexLockService {
    
    public Product getHotProduct(Long productId) {
        String key = "product:" + productId;
        String lockKey = "lock:product:" + productId;
        
        // 1. 查询缓存
        Product product = (Product) redisTemplate.opsForValue().get(key);
        if (product != null) {
            return product;
        }
        
        // 2. 尝试获取锁
        Boolean lockAcquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        if (lockAcquired) {
            try {
                // 3. 双重检查
                product = (Product) redisTemplate.opsForValue().get(key);
                if (product != null) {
                    return product;
                }
                
                // 4. 查询数据库
                product = productMapper.selectById(productId);
                if (product != null) {
                    redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
                }
                
                return product;
                
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 获取锁失败,等待后重试
            try {
                Thread.sleep(100);
                return getHotProduct(productId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return null;
            }
        }
    }
}

解决方案2:逻辑过期

@Data
public class CacheData<T> {
    private T data;
    private LocalDateTime expireTime;
}

@Service
public class LogicalExpireService {
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    public Product getHotProduct(Long productId) {
        String key = "product:" + productId;
        
        // 1. 查询缓存
        CacheData<Product> cacheData = (CacheData<Product>) redisTemplate.opsForValue().get(key);
        
        if (cacheData == null) {
            return queryAndCache(productId);
        }
        
        // 2. 检查逻辑过期时间
        if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
            // 未过期,直接返回
            return cacheData.getData();
        }
        
        // 3. 已过期,尝试获取锁
        String lockKey = "lock:product:" + productId;
        Boolean lockAcquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        if (lockAcquired) {
            // 4. 异步更新缓存
            executor.submit(() -> {
                try {
                    Product product = productMapper.selectById(productId);
                    if (product != null) {
                        CacheData<Product> newCacheData = new CacheData<>();
                        newCacheData.setData(product);
                        newCacheData.setExpireTime(LocalDateTime.now().plusMinutes(30));
                        
                        redisTemplate.opsForValue().set(key, newCacheData);
                    }
                } finally {
                    redisTemplate.delete(lockKey);
                }
            });
        }
        
        // 5. 返回过期数据(保证可用性)
        return cacheData.getData();
    }
}

缓存雪崩

面试官: "什么是缓存雪崩?如何预防?"

解决方案:

@Service
public class CacheAvalancheSolution {
    
    /**
     * 方案1:随机过期时间
     */
    public void setRandomExpire(String key, Object value, int baseMinutes) {
        // 基础时间 + 随机时间(0-10分钟)
        int randomMinutes = new Random().nextInt(10);
        int totalMinutes = baseMinutes + randomMinutes;
        
        redisTemplate.opsForValue().set(key, value, totalMinutes, TimeUnit.MINUTES);
    }
    
    /**
     * 方案2:多级缓存
     */
    @Autowired
    private CacheManager localCacheManager;
    
    public Product getProductWithMultiLevel(Long productId) {
        String key = "product:" + productId;
        
        // 1. 本地缓存
        Cache localCache = localCacheManager.getCache("products");
        Product product = localCache.get(productId, Product.class);
        if (product != null) {
            return product;
        }
        
        // 2. Redis缓存
        product = (Product) redisTemplate.opsForValue().get(key);
        if (product != null) {
            localCache.put(productId, product);
            return product;
        }
        
        // 3. 数据库
        product = productMapper.selectById(productId);
        if (product != null) {
            localCache.put(productId, product);
            setRandomExpire(key, product, 30);
        }
        
        return product;
    }
    
    /**
     * 方案3:缓存预热
     */
    @Scheduled(fixedRate = 25 * 60 * 1000) // 每25分钟执行一次
    public void cacheWarmUp() {
        Set<String> hotKeys = getHotKeys();
        
        for (String key : hotKeys) {
            Long ttl = redisTemplate.getExpire(key, TimeUnit.MINUTES);
            if (ttl != null && ttl < 5) { // 5分钟内过期
                refreshCache(key);
            }
        }
    }
}

🔒 分布式锁实现

Redis分布式锁

面试官: "如何用Redis实现分布式锁?有什么问题需要注意?"

基础实现:

@Component
public class RedisDistributedLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "lock:";
    
    /**
     * 获取锁
     */
    public boolean tryLock(String key, String value, int expireTime) {
        String lockKey = LOCK_PREFIX + key;
        
        // 使用SET NX EX命令,原子性操作
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
        
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放锁(Lua脚本保证原子性)
     */
    public boolean releaseLock(String key, String value) {
        String lockKey = LOCK_PREFIX + key;
        
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, 
            Collections.singletonList(lockKey), value);
        
        return Long.valueOf(1).equals(result);
    }
}

可重入锁实现:

@Component
public class ReentrantRedisLock {
    
    private static final ThreadLocal<String> LOCK_VALUE = new ThreadLocal<>();
    
    public boolean tryLock(String key, int expireTime) {
        String lockKey = "reentrant_lock:" + key;
        String value = generateLockValue();
        
        // Lua脚本实现可重入逻辑
        String luaScript = 
            "local key = KEYS[1] " +
            "local value = ARGV[1] " +
            "local expire = ARGV[2] " +
            "local current = redis.call('hget', key, 'value') " +
            "if current == false then " +
            "    redis.call('hset', key, 'value', value) " +
            "    redis.call('hset', key, 'count', 1) " +
            "    redis.call('expire', key, expire) " +
            "    return 1 " +
            "elseif current == value then " +
            "    redis.call('hincrby', key, 'count', 1) " +
            "    redis.call('expire', key, expire) " +
            "    return 1 " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, 
            Collections.singletonList(lockKey), 
            value, String.valueOf(expireTime));
        
        if (Long.valueOf(1).equals(result)) {
            LOCK_VALUE.set(value);
            return true;
        }
        
        return false;
    }
    
    private String generateLockValue() {
        return Thread.currentThread().getId() + ":" + System.currentTimeMillis();
    }
}

🔥 热点数据处理

热点检测

面试官: "如何检测和处理热点数据?"

热点检测实现:

@Component
public class HotKeyDetector {
    
    // 使用LRU缓存统计访问频率
    private final Cache<String, AtomicLong> keyAccessCount = 
        Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();
    
    private static final long HOT_THRESHOLD = 100;
    
    public void recordAccess(String key) {
        keyAccessCount.get(key, k -> new AtomicLong(0)).incrementAndGet();
    }
    
    public boolean isHotKey(String key) {
        AtomicLong count = keyAccessCount.getIfPresent(key);
        return count != null && count.get() >= HOT_THRESHOLD;
    }
    
    public List<String> getHotKeys() {
        return keyAccessCount.asMap().entrySet().stream()
            .filter(entry -> entry.getValue().get() >= HOT_THRESHOLD)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
    }
}

热点数据处理策略

@Service
public class HotDataService {
    
    // 本地缓存
    private final Cache<String, Object> localCache = 
        Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(2, TimeUnit.MINUTES)
            .build();
    
    /**
     * 本地缓存 + Redis缓存
     */
    public Object getHotData(String key) {
        hotKeyDetector.recordAccess(key);
        
        // 1. 检查本地缓存
        Object localValue = localCache.getIfPresent(key);
        if (localValue != null) {
            return localValue;
        }
        
        // 2. 检查Redis缓存
        Object redisValue = redisTemplate.opsForValue().get(key);
        if (redisValue != null) {
            // 如果是热点数据,放入本地缓存
            if (hotKeyDetector.isHotKey(key)) {
                localCache.put(key, redisValue);
            }
            return redisValue;
        }
        
        // 3. 查询数据库
        Object dbValue = queryFromDatabase(key);
        if (dbValue != null) {
            redisTemplate.opsForValue().set(key, dbValue, 30, TimeUnit.MINUTES);
            
            if (hotKeyDetector.isHotKey(key)) {
                localCache.put(key, dbValue);
            }
        }
        
        return dbValue;
    }
}

💡 面试回答要点

标准回答模板

缓存三大问题:

"缓存穿透是查询不存在数据导致请求打到数据库,
解决方案是缓存空值或使用布隆过滤器。

缓存击穿是热点数据过期瞬间大量请求打到数据库,
解决方案是互斥锁或逻辑过期。

缓存雪崩是大量缓存同时过期导致数据库压力过大,
解决方案是随机过期时间、多级缓存、缓存预热。"

分布式锁问题:

"Redis分布式锁需要注意原子性、过期时间、锁释放。
使用SET NX EX命令获取锁,Lua脚本释放锁保证原子性。
可重入锁通过Hash结构记录重入次数。
还要考虑锁续期、死锁检测等问题。"

本节核心要点:

  • ✅ 缓存穿透、击穿、雪崩的解决方案
  • ✅ Redis分布式锁的实现和优化
  • ✅ 热点数据的检测和处理策略
  • ✅ 实际项目中的缓存设计经验

总结: 缓存设计模式是高并发系统的核心,需要深入理解各种问题的本质和解决方案,在实际项目中灵活运用

Java面试圣经 文章被收录于专栏

Java面试圣经

全部评论

相关推荐

08-08 10:24
已编辑
中山大学 自动化
投递阿里巴巴集团等公司10个岗位
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务