天天看点

Redis缓存雪崩、缓存穿透、缓存击穿解决方案详解(下)3 缓存击穿(Hotspot Invalid)

3 缓存击穿(Hotspot Invalid)

  • 击穿针对的是某一个key缓存
  • 而雪崩是很多key

某key失效时,正好有高并发请求访问该key。

通常使用【缓存 + 过期时间】帮助我们加速接口访问速度,减少后端负载,同时保证功能的更新,一般情况下这种模式已基本满足需求。

但若同时出现如下问题,可能对系统十分致命:

  • 热点key,访问量非常大

    比如秒杀时。

  • 缓存的构建需要时间(可能是个复杂过程,例如复杂SQL、多次I/O、多个接口依赖)

于是就会导致:

在缓存失效瞬间,有大量线程构建缓存,导致后端负载加剧,甚至可能让系统崩溃。

Redis缓存雪崩、缓存穿透、缓存击穿解决方案详解(下)3 缓存击穿(Hotspot Invalid)

某些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

缓存为准

使用异步线程负责维护缓存的数据,定期或根据条件触发更新,这样就不会触发更新。

参考