3 缓存击穿(Hotspot Invalid)
- 击穿针对的是某一个key缓存
- 而雪崩是很多key
某key失效时,正好有高并发请求访问该key。
通常使用【缓存 + 过期时间】帮助我们加速接口访问速度,减少后端负载,同时保证功能的更新,一般情况下这种模式已基本满足需求。
但若同时出现如下问题,可能对系统十分致命:
-
热点key,访问量非常大
比如秒杀时。
- 缓存的构建需要时间(可能是个复杂过程,例如复杂SQL、多次I/O、多个接口依赖)
于是就会导致:
在缓存失效瞬间,有大量线程构建缓存,导致后端负载加剧,甚至可能让系统崩溃。
某些Key属极端热点数据,并发量很大情况下,如果这个Key过期,可能会在某个瞬间出现大量的并发请求同时回源,相当于大量的并发请求直接打到了数据库。这就是缓存击穿或缓存并发问题。
解决方案
考虑使用锁限制回源的并发。
如下代码示例,使用Redisson来获取一个基于Redis的分布式锁,在查询DB前先尝试获取锁:
@Autowired
private RedissonClient redissonClient;
@GetMapping("right")
public String right() {
String data = stringRedisTemplate.opsForValue().get("hotsopt");
if (StringUtils.isEmpty(data)) {
RLock locker = redissonClient.getLock("locker");
// 获取分布式锁
if (locker.tryLock()) {
try {
data = stringRedisTemplate.opsForValue().get("hotsopt");
// 双重检查,因为可能已经有一个B线程过了第一次判断,在等锁,然后A线程已经把数据写入了Redis中
if (StringUtils.isEmpty(data)) {
// 回源到数据库查询
data = getExpensiveData();
stringRedisTemplate.opsForValue().set("hotsopt", data, 5, TimeUnit.SECONDS);
}
} finally {
// 别忘记释放,另外注意写法,获取锁后整段代码try+finally,确保unlock万无一失
locker.unlock();
}
}
}
return data;
}
这样,可以把回源到数据库的并发限制在1。
在真实的业务场景下,不一定要这么严格地使用双重检查分布式锁进行全局的并发限制,因为这样虽然可以把数据库回源并发降到最低,但也限制了缓存失效时的并发。
所以可以考虑:
使用进程内的锁进行限制
这样每个节点都可以以一个并发回源DB。
Semaphore
限制并发数,比如限制为10,这样既限制了回源并发数不至于太大,又能使得一定量的线程可以同时回源。
永不过期
从 redis 上看,确实没有设置过期时间。这就保证不会出现热点 key 过期,即 “物理” 不过期。
“逻辑” 过期
功能上看,若不过期,不就成静态数据了?
所以我们把过期时间存在 key 对应的 value。若发现要过期了,通过一个后台异步线程进行缓存构建,即 “逻辑” 过期。
服务降级
hystrix
缓存为准
使用异步线程负责维护缓存的数据,定期或根据条件触发更新,这样就不会触发更新。
参考