前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
基础篇:
- Redis(一)
- Redis(二)
- Redis(三)
- Redis(四)
- Redis(五)
- Redis(六)
- Redis(七)
- Redis(八)
进阶篇:
- Redis(九)
- Redis(十)
- Redis(十一)
- Redis(十二)
接上期内容:上期完成了相关案例的学习。下面学习缓存穿透、预热、雪崩、击穿,话不多说,直接发车。
一、缓存预热
(一)、定义
缓存预热是一种在系统启动阶段或者特定时间点,将一些经常访问或者关键的数据提前加载到缓存中的操作,以减少对数据源(如数据库)的访问次数,从而提高系统的响应速度和性能。避免在用户首次请求时才去加载数据而导致的性能延迟。
(二)、功能
- 减少首次请求延迟:当用户首次访问某些数据时,如果没有进行缓存预热,系统需要从数据库等数据源中查询数据,这个过程可能会比较耗时。
- 减轻数据库压力:在系统运行初期,如果大量用户同时发起请求,这些请求都直接访问数据库,会给数据库带来巨大的压力,甚至可能导致数据库性能下降或者崩溃。缓存预热可以将部分数据提前加载到缓存中,后续的请求优先从缓存中获取数据,从而减少了对数据库的访问频率,降低了数据库的负载
- 提高系统性能和稳定性:由于缓存的读写速度通常比数据库等数据源快很多,缓存预热可以让系统在处理请求时更快地获取数据,从而提高系统的整体性能。同时,减少了对数据库的依赖,降低了因数据库故障或性能问题导致系统不可用的风险,增强了系统的稳定性。
(三)、常用方案
- 硬编码(不大推荐):在代码中直接明确地指定需要加载到缓存的数据和逻辑,不通过外部配置或动态计算来改变。在系统启动时,按照预先编写好的代码逻辑将数据加载到缓存中。
- @PostConstruct注解: Java 中的一个注解,用于标记一个方法,该方法会在依赖注入完成之后、对象正式投入使用之前被自动调用。
- 定时器任务:通过定时任务框架(如 Spring 的@Scheduled注解、Quartz 等),按照预设的时间间隔(如每天凌晨、每小时等)自动执行缓存预热操作。
- 数据脚本:使用脚本语言(如 Python、Shell 等)编写脚本,通过脚本连接到缓存系统,将数据加载到缓存中。
二、缓存雪崩
(一)、名词解释
缓存雪崩是指在某一时刻,缓存中大量的键在同一时间点或者在极短的时间内集中过期失效,或者缓存服务器发生故障导致缓存服务不可用,此时大量原本可以从缓存中获取数据的请求,都直接涌向了数据库等后端数据源,给数据库带来巨大的压力,甚至可能导致数据库不堪重负而崩溃,进而使整个系统出现性能急剧下降、服务不可用等严重问题。
(二)、发生场景
1、硬件方面
缓存服务器的硬件设备(如硬盘、内存、网卡等)出现故障,可能会导致缓存服务无法正常运行,从而发生缓存雪崩现象。
2、业务方面
大量的业务key同时过期,比如在进行缓存预热时,为大量缓存数据设置了相同的过期时间,当这个过期时间到达时,这些缓存数据会同时失效,从而引起缓存雪崩现象。
(三)、预防与解决措施
硬件方面无法把控,主要从业务方面来解决。
业务方面:
-
避免大量缓存键同时过期:①、设置随机时间:在设置缓存键的过期时间时,为每个键的过期时间添加一个随机的偏移量,避免它们集中在同一时刻过期。②、设置key用不过期。
-
redis集群实现服务高可用:使用缓存服务器的集群模式,集群模式可以将数据分散存储在多个节点上,当某个节点出现故障时,其他节点仍然可以正常提供服务,保证缓存服务的可用性。
-
多缓存结合:采用多级缓存架构,例如同时使用本地缓存(ehcache)+redis缓存。
-
服务降级:在应用层对请求进行限流,当请求量超过一定阈值时,直接拒绝部分请求,避免过多的请求直接访问数据库。
三、缓存穿透
(一)、名词解释
缓存穿透是指客户端请求的数据在缓存中不存在,同时在数据库中也不存在,这样每次该请求都会穿透缓存,直接访问数据库。如果有大量这样的无效请求持续涌入,会对数据库造成极大的压力,甚至可能导致数据库不堪重负而崩溃。例如,黑客可能会故意发起大量不存在的键的请求,以消耗数据库资源。
(二)、发生场景
- 黑客恶意攻击:攻击者可能会利用系统的漏洞,构造大量不存在的请求,如不存在的用户 ID、商品 ID 等,向系统发起请求。由于这些请求对应的数据在缓存和数据库中都不存在,会导致大量请求直接穿透缓存访问数据库,从而影响系统的正常运行。
- 业务数据异常:在业务系统中,可能会出现数据不一致或者数据删除不及时的情况。
- 错误的用户输入:如果系统没有对用户输入进行严格的验证,用户可能会输入错误的查询条件,如输入一个不存在的订单号、手机号码等。这些无效请求会直接穿透缓存访问数据库。
(三)、预防与解决措施
1、方案一
缓存空值或默认值,当查询的数据在数据库中不存在时,在缓存中存储一个空值或者默认值,并设置一个较短的过期时间。这样下次相同的请求就可以直接从缓存中获取空值或默认值,而不会再穿透到数据库。
2、方案二
使用布隆过滤器,①、自研布隆过滤器。②、使用Google Guava 库实现布隆过滤器。
(四)、案例演示
自研简略版布隆过滤器在上一篇已经学习过了,下面将学习Google Guava实现方式。Guava源码地址:GitHub - google/guava: Google core libraries for Java
1、需求说明
模拟使用Guava布隆过滤器拦截掉非法数字,对于合法的数字放行。
2、导入依赖
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.2-jre</version>
</dependency>
3、编码实现
public class GuavaBloomFilterDemo {public static void main(String[] args) {//误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)//fpp the desired false positive probability 0.0 < fpp < 1.0,误判率越低,消耗的资源越多,哈希函数也用的越多,误判率也就越低// 默认值 0.03BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 1000000, 0.03);//1 先往布隆过滤器里面插入100万的样本数据for (int i = 1; i <= 1000000; i++) {bloomFilter.put(i);}List<Integer> list1 = Arrays.asList(120000000, 111111, 10, 222222222, 42575, 123457);list1.forEach(e -> {boolean result = bloomFilter.mightContain(e);if (result) {// TODO 查redis → 查数据库 → .....System.out.println("存在,放行" + e);} else {// 直接返回结果System.out.println("不存在,拦截" + e);}});//故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里List<Integer> list = new ArrayList<>(1000000);for (int i = 1000000 + 1; i <= 1100000; i++) {if (bloomFilter.mightContain(i)) {list.add(i);}}System.out.println("误判的总数量::{}" + list.size());}
}
四、缓存击穿
(一)、名词解释
缓存击穿是指在高并发的场景下,一个非常热点的 key 在缓存中过期失效的瞬间,大量针对该 key 的请求同时涌入。由于此时缓存中没有该 key 对应的数据,这些请求就会全部转向数据库去查询。数据库在短时间内需要处理大量的查询请求,从而承受巨大的压力,可能会导致数据库性能急剧下降,甚至出现崩溃的情况,进而影响整个系统的正常运行。
(二)、发生场景
- 热点数据过期:在电商系统中,一款热门商品的信息会被大量用户频繁访问,为了减轻数据库压力,会将该商品信息缓存起来并设置过期时间。当这个过期时间到达,缓存中的数据失效,而此时恰好有大量用户同时发起对该商品信息的请求,就会出现缓存击穿的情况。
- 流量突发:一些突发的热点事件会导致瞬间产生大量的请求。比如,某明星突然发布一条微博,引发大量粉丝同时访问其个人主页,而该主页信息在缓存中过期,大量请求就会直接冲向数据库。
- 数据预热不正确:比如新闻网站在每天早上进行缓存预热,但遗漏了当天的一条重大热点新闻,当用户大量访问该新闻时,就会出现问题。
(三)、预防与解决措施
1、方案一
使用双检加锁策略。前面已经学习过了,不在阐述。
public String get(String key) {String value = redis.get(key);// 查询缓存if (value != null) {//缓存存在直接返回return value;} else {//缓存不存在则对方法加锁//假设请求量很大,缓存过期synchronized (this) {value = redis.get(key); // 在查一遍redisif (value == null) {// 从数据库获取数据value = dao.get(key);// 设置过期时间并回写到缓存redis.setex(key, time, value);}return value;}}}
2、方案二
随机退避策略。当发现缓存中热点 key 失效时,让各个请求不要立即去访问数据库,而是各自随机等待一段不同的时间后再去尝试获取数据。这样可以避免大量请求在同一时刻集中访问数据库,将请求的时间分散开,减轻数据库在短时间内的压力。
public String randomBackoffMethod(String key) {try {Jedis jedis = RedisUtils.getJedis();String data = jedis.get(key);if (data == null) {try {// 生成随机退避时间 毫秒int backoffTime = new Random().nextInt(2000);Thread.sleep(backoffTime);// 再次尝试从缓存获取数据,// 但是在高并发场景下,可能线程随机退避时间会一样,// 为了避免造成缓存双写不一致问题,使用双检锁策略来防止String value = jedis.get(key);// 查询缓存if (value != null) {//缓存存在直接返回return value;} else {//缓存不存在则对方法加锁//假设请求量很大,缓存过期synchronized (this) {value = jedis.get(key); // 在查一遍redisif (value == null) {// 从数据库获取数据value = dao.get(key);// 设置过期时间并回写到缓存jedis.setex(key, time, value);}return value;}}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}jedis.close();return data;} catch (Exception e) {e.printStackTrace();}return null;}
3、方案三
差异失效策略。它的核心思想是避免多个热点 Key 在同一时刻同时失效,从而防止大量请求在瞬间全部涌向数据库,给数据库造成过大压力。
在缓存击穿场景中的实现方式为一个热点key拷贝两份,两份缓存过期时间不一样,将缓存失效的时间分散开来,以此保障系统的稳定性与性能。
/*** 差异失效策略查询*/public String differentialFailureSelectMethod(String key) {// 同一个热点key的前缀String prefixA = "keyA:";String prefixB = "keyB:";try {Jedis jedis = RedisUtils.getJedis();String data = jedis.get(prefixA + key);if (data == null) {System.out.println("=========A缓存已经失效");//用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存Bdata = jedis.get(prefixB + key);if (data == null) {System.out.println("=========B缓存已经失效");//TODO 查数据库 → 回写redis}return data;}System.out.println("查询结果:{}" + data);} catch (Exception ex) {ex.printStackTrace();}return null;}/*** 差异失效策略更新*/public void differentialFailureUpdateMethod(String key) {try {// 以前的某个热点keyString oldHotKeyA = "oldHotKey:" + key;String oldHotKeyB = "oldHotKey:" + key;// 新热点key前缀String prefixA = "newKeyA:";String prefixB = "newKeyB:";//模拟从数据库查数据Jedis jedis = RedisUtils.getJedis();Object o = dao.get(key);//先更新B缓存jedis.del(oldHotKeyB);jedis.set(prefixB + o.getId());jedis.expire(prefixB + o.getId(), 2000);//再更新A缓存jedis.del(oldHotKeyA);jedis.set(prefixA + o.getId());jedis.expire(prefixA + o.getId(), 1000);} catch (Exception e) {e.printStackTrace();}}
五、总结
一图总结,四个问题以及解决办法:
问题类型 | 核心问题 | 解决方案 | 典型场景 |
---|---|---|---|
缓存预热 | 数据未提前加载 | 启动加载、定时任务、脚本 | 电商大促前的商品信息加载 |
缓存雪崩 | 大量缓存失效 | 随机过期、集群、高可用、限流 | 大量key过期、缓存服务器宕机时 |
缓存穿透 | 无效请求攻击 | 布隆过滤器、缓存空值 | 恶意攻击场景 |
缓存击穿 | 热点 Key 失效 | 双检锁、差异失效、随机退避 | 秒杀活动中的商品信息访问 |
ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。