目录
[1. 缓存穿透](#1. 缓存穿透)
[1.1. 简介](#1.1. 简介)
[1.2. 解决方案](#1.2. 解决方案)
[1.3. 总结](#1.3. 总结)
[2. 缓存雪崩](#2. 缓存雪崩)
[2.1. 简介](#2.1. 简介)
[2.2. 解决方案](#2.2. 解决方案)
[2.3. 总结](#2.3. 总结)
[3. 缓存击穿](#3. 缓存击穿)
[3.1. 简介](#3.1. 简介)
[3.2. 解决方案](#3.2. 解决方案)
[3.3. 总结](#3.3. 总结)
[4. 经典三缓存问题出现的根本原因](#4. 经典三缓存问题出现的根本原因)
[1. 缓存预热](#1. 缓存预热)
[1.1. 简介](#1.1. 简介)
[1.2. 作用和目的](#1.2. 作用和目的)
[2. 缓存清除](#2. 缓存清除)
[2.1. 什么是缓存清除?](#2.1. 什么是缓存清除?)
[2.2. 缓存清除的作用和目的](#2.2. 缓存清除的作用和目的)
[3. 代码示例](#3. 代码示例)
[4. 知识小结](#4. 知识小结)
一、缓存介绍
缓存是一种数据存储技术,用于存储经常访问的数据,以便在需要时快速获取。通过缓存数据,可以减少
数据的访问时间和系统的负载,从而提高应用程序的性能。缓存可以应用在多个层次,例如CPU缓存、数
据库缓存和Web缓存。
二、经典三缓存问题
1. 缓存穿透
1.1. 简介
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。
这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空
(相当于进行了两次无用的查询)
这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
1.2. 解决方案
解决方案:
- 对空值进行缓存
类似于上面的例子,虽然数据库中没有id=-9527的用户的数据,但是在redis中对他进行缓存(key=-9527,value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上。 - 实时监控
对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员对访问对象和访问数据进行分析查询,从而进行黑名单的设置限制服务。 - 使用布隆过滤器
使用BitMap作为布隆过滤器,将目前所有可以访问到的资源通过简单的映射关系放入到布隆过滤器中(哈希计算),当一个请求来临的时候先进行布隆过滤器的判断,如果有那么才进行放行,否则就直接拦截。 - 接口校验
类似于用户权限的拦截,对于id=-3872这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。
1.3. 总结
举例:客户端发送大量的不可响应的请求(如下图)
根本原因(结合上文)就是:请求根本不存在的资源

当大量的客户端发出类似于:https://localhost: 9090/user/18933?id=-9527的请求,就可能导致出现
缓存穿透的情况。
因为数据库DB中本身就没有id=-9527的用户的数据,所以Redis也没有对应的数据,那么这些请求在redis
就得不到响应,就会直接打在DB上,导致DB压力过大而卡死情景在线或宕机
2. 缓存雪崩
2.1. 简介
我们可以简单的理解为:由于原有缓存失效,新缓存未到时间(例如:我们设置缓存时采用了相同的过期时
间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库
CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
2.2. 解决方案
解决方案:
- 将失效时间分散开
通过使用自动生成随机数使得key的过期时间是随机的,防止集体过期 - 使用多级架构
使用nginx缓存+redis缓存+其他缓存,不同层使用不同的缓存,可靠性更强 - 设置缓存标记
记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际的key - 使用锁或者队列的方式
如果查不到就加上排它锁,其他请求只能进行等待
2.3. 总结
产生的原因:redis中大量的key集体过期
比如:当redis中的大量key集体过期,可以理解为redis中的大部分数据都被清空了(失效了),那么这时
候如果有大量并发的请求来到,那么redis就无法进行有效的响应(命中率急剧下降),请求就都打到DB上
了,到时DB直接崩溃。
3. 缓存击穿
3.1. 简介
某个 key 非常非常热,访问非常的频繁,高并发访问的情况下,当这个 key在失效(可能expire过期了,
也可能LRU淘汰了)的瞬间,大量的请求进来,这时候就击穿了缓存,直接请求到了数据库,一下子来这
么多,数据库肯定受不了,这就叫缓存击穿。某个key突然失效,然后这时候高并发来访问这个key,结果
缓存里没有,都跑到db了。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都
过期了,很多数据都查不到从而查数据库。
3.2. 解决方案
解决方案:
- 提前对热点数据进行设置
类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中 - 监控数据,适时调整
监控哪些数据是热门数据,实时的调整key的过期时长 - 使用锁机制
只有一个请求可以获取到互斥锁,然后到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应
3.3. 总结
产生的原因:redis中的某个热点key过期,但是此时有大量的用户访问该过期key。
比如:类似于"某明星出轨事件"上了热搜,这时候大量的"粉丝"都在访问该热点事件,但是可能由于某种原
因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求
直接打在DB服务器上,导致整个DB瘫痪。
4. 经典三缓存问题出现的根本原因
三者出现的根本原因是:Redis缓存命中率下降,请求直接打到DB上了
正常情况下,大量的资源请求都会被redis响应,在redis得不到响应的小部分请求才会去请求DB,这样DB
的压力是非常小的,是可以正常工作的

如果大量的请求在redis上得不到响应,那么就会导致这些请求会直接去访问DB,导致DB的压力瞬间变大
而卡死或者宕机。
大量的高并发的请求打在redis上
这些请求发现redis上并没有需要请求的资源,redis命中率降低
因此这些大量的高并发请求转向DB(数据库服务器)请求对应的资源
DB压力瞬间增大,直接将DB打垮,进而引发一系列"灾害"

三、常见双缓存方案
1. 缓存预热
1.1. 简介
缓存预热是一种在应用程序启动或缓存失效之后,主动将热点数据加载到缓存中的策略。
这样,在实际请求到达应用程序时,热点数据已经存在于缓存中,从而减少了缓存未命中的情况,提高了
应用程序的响应速度。
1.2. 作用和目的
缓存预热的主要作用和目的如下:
- 提高缓存命中率:通过预先加载热点数据,缓存预热可以提高缓存的命中率,从而减少对后端数据源(如数据库)的访问,降低系统的负载。
- 保持应用程序性能稳定:在应用程序启动或缓存失效之后,缓存预热可以防止请求对后端数据源产生突然的压力,从而保持应用程序的性能稳定。
- 优化用户体验:由于热点数据已经存在于缓存中,用户在请求这些数据时能获得更快的响应速度,从而提高用户体验。
2. 缓存清除
2.1. 什么是缓存清除?
缓存清除是一种策略,用于在数据发生变化时删除或更新缓存中的相关数据,以确保缓存中的数据与数据
源保持一致。缓存清除可以是手动触发的,也可以是自动触发的,例如设置缓存的过期时间。
2.2. 缓存清除的作用和目的
缓存清除的主要作用和目的如下:
- 保持数据一致性:当数据发生变化时,缓存清除可以确保缓存中的数据与数据源保持一致,从而避免因缓存数据过期或错误而导致的应用程序错误。
- 释放缓存空间:缓存空间是有限的,缓存清除可以删除不再需要的数据,为新的数据访问腾出空间。
- 提高缓存利用率:通过删除过期或不常用的数据,缓存清除可以确保缓存中的数据是最有价值的,从而提高缓存的利用率。
- 避免脏数据:在某些情况下,缓存中的数据可能因为程序错误、系统故障等原因而变得不可靠。
缓存清除可以定期或根据需要清理这些脏数据,确保缓存中的数据是有效的。
3. 代码示例
我们使用Spring Boot的spring-boot-starter-cache依赖来实现缓存。
首先,在pom.xml中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
假设我们有一个表示商品的Product实体:
public class Product {
private Long id;
private String name;
private BigDecimal price;
// 构造函数、getter和setter方法
}
我们创建一个ProductService类来处理Product实体的查询和更新操作,
并使用@Cacheable和@CacheEvict注解来实现缓存预热和缓存清除。
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
// 模拟数据库查询
return productRepository.findById(id);
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
// 模拟数据库更新
productRepository.update(product);
}
// 缓存预热方法
public void cacheWarmUp() {
List<Product> products = productRepository.findAll();
for (Product product : products) {
getProduct(product.getId());
}
}
}
在这个示例中,我们使用@Cacheable注解来缓存getProduct方法的结果,当获取商品时,首先会在缓存
中查找,如果未找到,则查询数据库并将结果存储到缓存中。我们使用@CacheEvict注解来清除
updateProduct方法更新的商品的缓存。cacheWarmUp方法用于缓存预热,它主动加载所有商品到缓存
中。要在应用程序启动时执行缓存预热,我们可以实现CommandLineRunner接口并重写run方法:
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class CacheWarmUpRunner implements CommandLineRunner {
private final ProductService productService;
public CacheWarmUpRunner(ProductService productService) {
this.productService = productService;
}
@Override
public void run(String... args) {
productService.cacheWarmUp();
}
}
现在,我们的应用程序在启动时将自动执行缓存预热,加载所有商品到缓存中。
同时,在更新商品时,相关缓存将被清除,以保持数据的一致性。
4. 知识小结
本节详细介绍了缓存预热和缓存清除的概念、原理、作用和目的。缓存预热是一种主动加载热点数据到缓
存的策略,旨在提高缓存命中率、保持应用程序性能稳定和优化用户体验。而缓存清除则是在数据发生变
化时删除或更新缓存中的相关数据,以保持数据一致性、释放缓存空间、提高缓存利用率和避免脏数据。