前言
用户数据通常存储在数据库中,而数据库的数据则保存在磁盘上。由于磁盘的读写很慢,为了避免用户直接访问数据库,我们可以使用 Redis 作为缓存层。而引入缓存层后,会出现三种缓存异常问题:缓存穿透 、缓存击穿 和缓存雪崩 ,这篇文章主要探讨缓存击穿 和缓存雪崩的问题。
想要了解缓存穿透及其解决方法,可以看我的这篇博客:【Redis】缓存穿透详解
1.缓存击穿
1.1 什么是缓存击穿
缓存击穿是指在缓存中没有命中数据(缓存不存在),并且该数据正好是一个热点数据(经常访问的、被频繁请求的数据)。由于缓存中没有该数据,系统会直接访问数据库,而在高并发的情况下,多个请求会同时穿透缓存,直接访问数据库,造成数据库压力骤增,可能导致数据库崩溃或系统性能下降。
1.2 缓存击穿的产生原因
缓存击穿通常发生在以下几种情况:
- 热点数据的缓存失效:当某个热点数据的缓存过期时,大量请求涌入到数据库层,而此时数据库需要处理所有的请求,造成数据库的瞬时压力增大。
- 缓存中没有数据:某些数据本身并没有被缓存,可能是由于缓存策略不当、数据没有被及时缓存,或者缓存过期。
1.3 如何解决缓存击穿
缓存击穿的核心问题是如何有效避免缓存失效后,多个请求直接击穿缓存访问数据库。
1.3.1 热点数据永不过期
对于特别重要的热点数据,可以考虑不设置缓存过期时间,让这些数据一直保存在缓存中。可以通过定时任务手动更新缓存中的数据来避免数据过期问题。
1.3.2 使用互斥锁(锁机制)
在缓存失效的情况下,采用分布式锁或互斥锁来确保只有一个线程/请求去加载数据库数据,并更新缓存。其他线程则等待,直到缓存被更新完毕。例如,可以通过 Redis 的 SETNX(set if not exists)命令来实现分布式锁。
java
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 获取分布式锁
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 10, TimeUnit.SECONDS)) {
try {
// Double-check
value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 查询数据库
value = database.get(key);
// 将结果写入缓存
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 等待锁释放后,再从缓存中读取数据
Thread.sleep(100); // 自行调整等待时间
value = redisTemplate.opsForValue().get(key);
}
}
1.3.3 预防性缓存更新
在热点数据即将过期时,提前异步刷新缓存。通过检测热点数据的访问频率,当即将过期时触发自动更新操作,避免过期瞬间的击穿问题。
2.缓存雪崩
2.1 什么是缓存雪崩
缓存雪崩是指缓存中的大量数据在短时间内同时过期,导致大量请求同时访问数据库,造成数据库的瞬间负载激增,可能导致数据库崩溃。缓存雪崩的问题通常发生在缓存中的数据都在同一时刻过期,尤其是缓存的过期时间相同或过期时间比较集中时。
2.2 缓存雪崩的产生原因
缓存雪崩通常是以下几种原因导致的:
- 缓存过期时间设计不当:多个缓存数据设置了相同的过期时间,导致它们在同一时刻过期,造成大量请求同时访问数据库。
- 缓存服务器宕机或故障:缓存服务器出现故障,所有缓存数据无法提供服务,导致请求直接访问数据库。
- 缓存容量不足:缓存空间不足,无法承载大量的数据,导致频繁失效和大量缓存穿透。
2.3 如何解决缓存雪崩
解决缓存雪崩的根本方法是避免大量缓存数据同时失效,或者通过其他手段避免请求直接访问数据库
2.3.1 设置不同的缓存过期时间
为避免缓存雪崩,最直接的解决办法是让缓存中的不同数据拥有不同的过期时间。可以为每个缓存项添加一定的随机值,避免缓存的失效时间集中在某一时刻。
java
// 为每个缓存设置一个不同的过期时间,带有随机偏移量
long randomOffset = ThreadLocalRandom.current().nextLong(0, 60000);
redisTemplate.opsForValue().set(cacheKey, value, baseExpireTime + randomOffset, TimeUnit.MILLISECONDS);
2.3.2 使用缓存预热
在系统启动时,或者通过某种定时机制对缓存进行预热。即提前加载一些关键数据到缓存中,避免在数据首次请求时缓存为空,从而导致直接访问数据库。
2.3.3 降级策略
在缓存雪崩时,可以采取限流、降级等策略,减缓数据库的压力。如在缓存失效时,直接返回默认值或缓存过期的旧数据,避免数据库短时间内处理大量请求。