一. 概述
缓存穿透:客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会访问数据库。导致DB的压力瞬间变大而卡死或者宕机。
缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
缓存击穿:Redis中的某个热点key过期,但是此时有大量的用户访问该过期key。
二.缓存穿透
1.产生原因:
- 请求不存在的数据: 当用户请求的数据在缓存和数据库中都不存在时,每次请求都要到数据库查询,然后返回空结果。
- 恶意攻击: 攻击者故意请求不存在的数据,导致数据库压力增大。
2.解决方法:
- 布隆过滤器: 在查询数据库之前,先通过布隆过滤器判断数据是否存在,如果不存在,则直接返回。
布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,假设布隆过滤器判断这个数据不存在,则直接返回
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突
- 缓存空结果: 对于查询结果为空的操作,可以将空结果缓存起来,这样相同的请求就会直接命中缓存。
当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了
- 设置过期时间: 对于空结果的缓存设置一个较短的过期时间,避免长时间占用缓存空间。
三.缓存雪崩
1.产生原因:
- 缓存同时失效: 当大量缓存在同一时间过期,导致大量请求同时到达数据库,造成数据库压力突增。
- 缓存服务不可用: 缓存服务宕机,导致所有请求都直接访问数据库。
2.解决方法:
- 缓存数据分级: 将缓存数据分级,不同级别的数据设置不同的过期时间,避免同时过期。
- 高可用架构: 使用分布式缓存系统,如Redis集群,提高缓存服务的可用性。
- 限流降级: 在数据库层面进行限流,避免数据库被压垮;同时提供降级方案,如返回默认值或错误信息。
四.缓存击穿
1..产生原因:
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
2.解决方法:
- 互斥锁: 对于热点数据的查询操作,在缓存中设置互斥锁,确保同一时间只有一个请求能查询数据库并回写缓存。
- 永不过期: 对于热点数据,可以设置缓存永不过期,或者设置一个很长的过期时间。
- 预热缓存: 在应用启动时,预先加载热点数据到缓存中。
2.1互斥锁
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
2.2 逻辑过期
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是旧数据。
2.3 互斥锁和逻辑过期对比
互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响
逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻