缓存击穿的成因 缓存击穿是指在高并发场景下,某个热点数据的缓存突然失效(如缓存过期),而这时恰好有大量的并发请求来访问这个刚刚失效的key,所有请求都无法从缓存中获取到数据,进而都涌向数据库,导致数据库瞬时压力过大,这就是所谓的“击穿”。尤其是在数据更新并不频繁的情况下,这种集中性的数据库查询压力可能导致数据库响应变慢,甚至宕机。 解决方案 - Java代码示例(使用Redis分布式锁) 下面是一个基于Redis实现分布式锁,用于解决缓存击穿问题的基本Java代码框架: import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import java.util.Collections; @Service public class CacheService { private final StringRedisTemplate redisTemplate; private final RedisScript<Long> luaLockScript; public CacheService(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; luaLockScript = new DefaultRedisScript<>(// 定义Lua脚本,用于获取分布式锁 "if redis.call('exists', KEYS[1]) == 0 then " "redis.call('hset', KEYS[1], ARGV[1], 1);" "redis.call('pexpire', KEYS[1], ARGV[2]); " "return 1; " "end;" "return 0;", Long.class); } public Object getDataFromDBWithLock(String cacheKey) { Boolean locked = acquireLock(cacheKey, "uniqueId"); // 尝试获取锁 if (locked) { try { // 如果获取到锁,则尝试从缓存中获取数据 Object data = getDataFromCache(cacheKey); if (data != null) { return data; } // 缓存未命中,从数据库加载数据 data = loadFromDatabase(cacheKey); // 将数据写入缓存 writeToCache(cacheKey, data); return data; } finally { releaseLock(cacheKey, "uniqueId"); // 无论何时,都要确保最后释放锁 } } else { // 没有获取到锁,等待其他线程完成数据库操作后从缓存中读取 return getDataFromCacheAfterWait(cacheKey); } } private Boolean acquireLock(String key, String uniqueId) { // 调用Lua脚本获取分布式锁,这里假设expireTime是你设置的锁超时时间 Long result = redisTemplate.execute(luaLockScript, Collections.singletonList(key), uniqueId, String.valueOf(expireTime)); return result == 1L; } private void releaseLock(String key, String uniqueId) { // 在实际应用中,释放锁可能涉及到更复杂的逻辑,比如判断持有锁的线程ID一致才能释放 // 这里简化处理,仅作示意 redisTemplate.delete(key); } // 其他辅助方法... private Object getDataFromCache(String cacheKey) { ... } private Object loadFromDatabase(String cacheKey) { ... } private void writeToCache(String cacheKey, Object data) { ... } } 上述代码是一个简化的示例,`loadFromDatabase` 和 `writeToCache` 方法需要根据实际的缓存和数据库操作进行实现。通过这种方式,当缓存失效时,只有一个线程能够获得锁并执行数据库查询,其他线程则等待锁释放后从缓存中读取数据,从而避免了数据库的并发压力。
缓存雪崩的成因 缓存雪崩通常是指缓存层出现了大规模的缓存失效,可能是由于以下原因导致的: 1. 大量缓存集中在同一时刻失效,比如设置了一致的过期时间,到期后大量缓存同时失效。 2. 缓存服务整体宕机,导致所有请求无法通过缓存,直接达到后端数据库。 3. 缓存数据的大规模删除,如误操作清空了大量缓存。 解决方案 1. 随机设置过期时间:让缓存的过期时间分散分布,而不是同时失效,可以减少同时失效带来的压力。 2. 缓存预热:在缓存重建前,提前将热门数据加载到缓存中。 3. 设置二级缓存:即使一级缓存失效,还有二级缓存作为备用,减轻数据库压力。 4. 使用互斥锁:当缓存失效时,只允许一个请求去数据库加载数据,其它请求等待锁释放后从缓存获取。 5. 服务熔断与降级:在缓存雪崩发生时,采取熔断措施,避免请求继续涌入数据库,同时提供降级服务。 6. 缓存高可用架构:搭建缓存集群,具备故障转移能力,防止单点故障引起的服务中断。 Java代码示例(伪代码,仅展示关键思路) #### 随机过期时间设置 import org.springframework.cache.annotation.Cacheable; import java.time.Duration; import java.util.concurrent.TimeUnit; @Service public class ProductService { @Cacheable(value = "products", key = "#id", condition = "#result != null", unless = "#result == null", sync = true, cacheManager = "cacheManager") // sync=true 表示同步方式获取缓存,避免并发时多次查询数据库 public Product getProductById(Long id) { Duration randomExpiration = Duration.ofSeconds(randomBetween(minExpireSecs, maxExpireSecs)); // 查询数据库并获取产品数据 Product product = queryProductFromDatabase(id); // 设置缓存时加上随机过期时间 cacheManager.getCache("products").put(id, product, randomExpiration.toMillis(), TimeUnit.MILLISECONDS); return product; } // ... // 内部实现randomBetween函数生成随机的过期时间区间 } 服务熔断与降级(借助Hystrix或其他熔断工具) import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; @Service public class ProductService { @HystrixCommand(fallbackMethod = "getFallbackProductById") @Cacheable("products") public Product getProductById(Long id) { // 正常查询逻辑 return queryProductFromDatabase(id); } // 当数据库压力过大时,提供降级服务 private Product getFallbackProductById(Long id) { log.error("Cache miss and fallback for productId: {}", id); // 提供默认或缓存的备份数据,或直接返回错误信息 return defaultProduct(); } // ... // 内部实现queryProductFromDatabase和defaultProduct方法 } 实际上,Java代码的具体实现会依赖于使用的具体缓存组件(如Redis、Memcached)以及熔断框架(如Hystrix、Resilience4j)。以上代码旨在表达解决缓存雪崩问题的核心思路,而非完整的生产环境下的代码实现。在实际项目中,还需要考虑更多细节,比如配置管理、并发控制、分布式锁的使用等。