缓存雪崩、缓存击穿、缓存穿透

一、缓存雪崩

缓存雪崩是指当缓存中的大量数据在同一时间失效,导致大量请求直接访问数据库,从而引起数据库的压力过大,严重影响系统的性能。解决缓存雪崩的常用方法有三种。

解决方案:

1、缓存数据的随机过期时间

一种解决缓存雪崩的方法是在缓存数据的过期时间上增加随机因素,从而避免大量数据在同一时间失效的情况。在SpringBoot中,我们可以通过设置Redis缓存的过期时间和一个随机值来实现这个解决方案。例如

@Override
public List<User> getUserList() {
     List<User> userList = redisTemplate.opsForValue().get("userList");
     if (userList == null) {
          // 如果Redis中不存在,则查询数据库
          userList = userDao.getUserList();
        if (userList != null && userList.size() > 0) {
          // 将数据缓存到Redis中,并增加随机的过期时间
         int random = new Random().nextInt(600) + 600;
         redisTemplate.opsForValue().set("userList", userList, random, TimeUnit.SECONDS);
        }
     }
     return userList;
}

在上面的代码中,我们先在缓存中查询数据,如果不存在,则去数据库中查询,并将数据缓存到Redis中,并增加随机的过期时间。这样即使大量数据在同一时间失效,也不会全部直接访问数据库,从而避免了缓存雪崩的问题。

2、预热缓存

另外一种解决缓存雪崩的方法是在系统启动时预热缓存,将系统中的热点数据提前加载到缓存中,从而避免了大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过编写一个启动时执行的方法,来实现预热缓存的解决方案。例如:

@Component
public class CacheInit implements CommandLineRunner {
    @Autowired
    private UserDao userDao;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void run(String... args) throws Exception {
        List<User> userList = userDao.getUserList();
        if (userList != null && userList.size() > 0) {
            // 将数据缓存到Redis中,并设置过期时间为1小时
            for (User user : userList) {
                redisTemplate.opsForValue().set(user.getId(), user, 1, TimeUnit.HOURS);
            }
        }
    }
}

在上面的代码中,我们在系统启动时执行run方法,在该方法中先去数据库中查询热点数据,然后将数据缓存到Redis中,并设置过期时间为1小时。这样即使缓存中的数据在同一时间失效,也能够保证系统中的热点数据始终被缓存,从而避免了缓存雪崩的问题。

3、使用分布式锁

最后一种解决缓存雪崩的方法是使用分布式锁,从而避免大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过Redisson来实现分布式锁的解决方案。例如:

@Override
    public List<User> getUserList() {
        List<User> userList = redisTemplate.opsForValue().get("userList");
        if (userList == null) {
            // 如果Redis中不存在,则尝试获取分布式锁
            RLock lock = redissonClient.getLock("userListLock");
            try {
                // 尝试加锁,并设置锁的过期时间为5秒
                boolean success = lock.tryLock(5, TimeUnit.SECONDS);
                if (success) {
                    // 如果获取到了锁,则查询数据库并将数据缓存到Redis中
                    userList = userDao.getUserList();
                    if (userList != null && userList.size() > 0) {
                        redisTemplate.opsForValue().set("userList", userList, 1, TimeUnit.HOURS);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                lock.unlock
            }
        } return userList;
    }

在上面的代码中,我们先在缓存中查询数据,如果不存在,则尝试获取分布式锁,如果获取到了锁,则查询数据库并将数据缓存到Redis中。如果没有获取到锁,则等待一段时间再尝试获取锁,这样即使大量请求同时访问系统,也能够保证只有一个请求去查询数据库并缓存数据,从而避免了缓存雪崩的问题。

二、缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无 数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的, 此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的 时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到 数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行 数据库代码,对数据库访问压力过大

1、互斥锁

如果未命中缓存,先获取互斥锁,获取锁之后要再次检查缓存,如果还是未命中进行缓存重建,这样当其他线程来的时候就会获取锁失败,这时我们让这个线程休眠一会,重新查询缓存,如果命中就返回嘛,如果没命中再次尝试获取锁,假设这次获取锁成功了,还是再次检查缓存,如果未命中重建缓存。

优点:可保证数据高一致性

缺点:性能低,可能发生死锁

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人 去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休 眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

2、逻辑过期

为缓存key设置逻辑过期时间(就是加一个字段),假设线程1查询缓存,未命中直接返回,命中判断是否过期发现,没过期也好说直接返回数据就行,已过期,就会尝试获取锁,然后此刻开启新的线程进行缓存重建,线程1返回旧数据,其他线程获取锁失败都返回旧数据。

优点:性能高

缺点:数据可能不一致,实现复杂

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续 通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1 去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据 的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访 问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把 重建数据构建完后,其他线程才能走返回正确的数据。

两者对比:

解决方案优点缺点
互斥锁

没有额外的内存消耗

保证一致性

实现简单

线程需要等待,性能受影响

可能有死锁风险

逻辑过期线程无需等待,性能较好

不保证一致性

有额外内存消耗

实现复杂

三、缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中没有数据,请求会直接穿透到数据库中,从而引起数据库的压力过大,严重影响系统的性能。解决缓存穿透的常用方法有两种。

1、布隆过滤器

布隆过滤器是一种高效的数据结构,可以判断一个元素是否存在于一个集合中,同时也可以减轻数据库的压力。在使用布隆过滤器的时候,首先将所有的数据hash到一个位图中,如果查询的数据在位图中不存在,那么直接返回不存在,从而避免了对数据库的查询操作。

优点:内存占用较少,没有多余key

缺点:实现复杂,存在误判可能

在SpringBoot中,我们可以使用Guava提供的布隆过滤器实现缓存穿透的解决方案。例如:

@Bean
public BloomFilter bloomFilter() {
    return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000, 0.001);
}

@Override
public User getUserById(String id) {
    // 先从布隆过滤器中查询是否存在
    if (!bloomFilter.mightContain(id)) {
        return null;
    }
    // 如果存在,则查询Redis中的缓存数据
    User user = redisTemplate.opsForValue().get(id);
    if (user == null) {
        // 如果Redis中不存在,则查询数据库
        user = userDao.getUserById(id);
        if (user != null) {
            // 将数据缓存到Redis中
            redisTemplate.opsForValue().set(id, user);
        } else {
            // 如果数据库中也不存在,则将该id加入到布隆过滤器中
            bloomFilter.put(id);
        }
    }
    return user;
}

在上面的代码中,首先通过布隆过滤器判断请求的数据是否存在于集合中,如果不存在,则直接返回null,从而避免了对数据库的查询操作。

2、空对象存储

另外一种解决缓存穿透的方法是采用空对象缓存的方式,即当查询的数据不存在时,将一个空对象缓存到Redis中。这样下次查询同样不存在的数据时,就可以直接从Redis中获取到一个空对象,从而避免了对数据库的查询操作。

优点:实现简单,方便维护

缺点:占内存,可能造成短期数据不一致

在SpringBoot中,我们可以通过设置Redis缓存的过期时间来实现空对象缓存的解决方案。例如:

@Override
public User getUserById(String id) {
    User user = redisTemplate.opsForValue().get(id);
    if (user == null) {
        // 如果Redis中不存在,则查询数据库
        user = userDao.getUserById(id);
        if (user != null) {
            // 将数据缓存到Redis中
            redisTemplate.opsForValue().set(id, user);
        } else {
            // 如果数据库中也不存在,则将一个空对象缓存到Redis中,设置过期时间防止缓存雪崩
            redisTemplate.opsForValue().set(id, new User(), 5, TimeUnit.MINUTES);
        }
    }
    return user;
}

在上面的代码中,当查询的数据不存在时,我们将一个空对象缓存到Redis中,并设置了5分钟的过期时间。这样即使缓存中的数据被清空了,也不会引起数据库的压力过大,从而避免了缓存穿透。

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值