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面试圣经