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 !=

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

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

Java面试圣经,带你练透java圣经

全部评论
欢迎讨论
点赞 回复 分享
发布于 2025-09-06 11:27 江西

相关推荐

牛客60022193...:大厂都招前端,他们觉得AI能替代前端,可能他们公司吊打btaj吧
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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