⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 孤独烟 「孤独烟」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

引言

今天,重新回顾一下缓存击穿这个问题! 之所以写这个文章呢,因为目前网上流传的文章落地性太差(什么布隆过滤器啊,布谷过滤器啊,嗯,你们懂的),其实这类方案并不适合在项目中直接落地。

那么,我们在项目中落地代码的时候,其实只需要一个注解就能解决这些问题,并不需要搞的那么复杂。

本文有一个前提,读者必须是java栈,且是用Springboot构建自己的项目,如果是go技术栈或者python技术栈的,可能介绍的思路仅供大家参考!

正文

目前缺陷

首先,为什么说目前网上流传的方案,落地性差呢,因为都缺乏一个可以和SpringBoot结合起来的真实场景,基本上都脱离了SpringBoot,只站在Java这个层级去分析。那问题就来了,现在还有不用SpringBoot的公司么?因此,本文尝试将该方案和SpringBoot结合起来,讲一个确实可行,可以落地的方案!

当然,我们先来说说目前在网上流传的几套方案,到底不靠谱在哪里!

(1)布隆过滤器 关于布隆过滤器,我就不介绍太多,这里就理解为是一个过滤器,用于快速检索一个元素是否在一个集合中;那么当一个请求来的时候,快速判断这个请求的key是否在指定集合中!如果在,说明有效,则放行。如果不在,则无效拦截。 至于实现,各大博客也说了用了google提供的

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>

这个包里有现成写好的java类给你使用了,当然demo代码我就不贴了,一抓一大把! 当然,似乎看上去完美无暇!一切都是那么的合适!

然而到这里,我就真的问一句,你们真的用了这个方案了?

我如果猜的没错,应该没几个人遇到过缓存击穿问题~更何况,证明这个说法的正确性~

该方案最大的一个问题是布隆过滤器不支持反向删除操作,例如你的项目里活跃的key的数量只有1000w个,但是全部key数量有5000w个,那这5000w个key会全部存在布隆过滤器里!

直到某一天,你会发现这个过滤器太拥挤了,误判率太高,不得不进行重建!

so,你们觉得这个做法真的靠谱?

那么布隆过滤器这个说法出自哪里呢? (大家一定很好奇对不对!)

当然是xx机构~~此处保护自己的狗头~~记住,他们为了割韭菜,一定会选择一些看起来极为高端,但是落地巨不靠谱的方案(这也是区分一个机构到底是割韭菜还是真正有水平的标杆,小白不懂,很容易被坑)~~看到这里,真是惭愧,我的第一篇文章也是写这个方案了,但是在落地过程中,发现了不对劲(此处省略一万字的检讨文,烟哥垃圾~~)。

(2)布谷过滤器 那么,为了解决布隆过滤器查询性能弱、空间利用效率低、不支持反向操作等问题,又有一篇文章诞生了,主张用布谷过滤器来解决缓存击穿问题!

但是,神奇的事情来了,基本上所有的文章都在说布谷过滤器多么多么牛逼,却没有任何落地的方案~

记住,我们平时写代码,一定是怎么方便怎么来!再记住,面试是一回事,代码落地是另一回事~

那,真正简便的方案是什么样的呢?来,我们一步步来~

真正方案

假设,你此刻用的是springboot-2.x的版本,你为了能够连接redis,你在pom文件里加入如下依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后呢,我们修改application.yml

spring:
datasource:
...
redis:
database: ...
host: ...
port: ...
(省事,不全贴了)

ok,说到这里,就不得不说一下spring-cache了,Spring3.1之后,引入了注解缓存技术,其本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加少量自定义的各种annotation,即能够达到使用缓存对象和缓存方法的返回对象的效果。Spring的缓存技术具备相当的灵活性,不仅能够使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存集成。

例如:我们在代码中经常有这么一段逻辑,在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值,不再执行目标方法;没有则执行目标方法,去数据库查询出对应的value,并以键值对的形式存入缓存。

如果我们不使用例如spring-cache的注解框架,你的代码中会充斥着大量冗余代码,而用了该框架后,以@Cacheable注解为例, 该注解在方法上,表示该方法的返回结果是可以缓存的。

也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法。

那么,你的代码只需要这么写

@Override
@Cacheable("menu")
public Menu findById(String id) {
Menu menu = this.getById(id);
if (menu != null){
System.out.println("menu.name = " + menu.getName());
}
return menu;
}

在这个例子中,findById 方法与一个名为 menu 的缓存关联起来了。调用该方法时,会检查 menu 缓存,如果缓存中有结果,就不会去执行方法了。

ok,说到这里,其实都是大家懂得东西!!接下来开始我们的主题:如何解决缓存击穿问题!顺便讲讲穿透和雪崩问题!

来来来,我们回忆一下缓存击穿,穿透以及缓存雪崩的概念!

缓存穿透

在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,如活动系统里面查询一个不存在的活动。 多嘴一句:缓存穿透是指,请求的是缓存和数据库中都没有的数据!

对于缓存穿透问题,有一个很简单的解决方案,就是缓存NULL值~从缓存取不到的数据,在数据库中也没有取到,直接返回空值。

那么spring-cache中,有一个配置是这样的

spring.cache.redis.cache-null-values=true

带上该配置后,就可以缓存null值了,值得一提的是,这个缓存时间要设的少一点,例如15秒就够,如果设置过长,会导致正常的缓存也无法使用。

缓存击穿

在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上,如活动系统里面查询活动信息,但是在活动进行过程中活动缓存突然过期了。 多嘴一句:缓存击穿是指,请求的是缓存没有,而数据库中有的数据!

记住,解决击穿的最简单的方法,只有一个,就是限流!至于怎么限,其实可以各显神通!例如其他文章提到的布隆过滤器,布谷过滤器等,不过是限流方式之一而已!甚至,你用一些其他的限流组件也是可以的!

这里就要说spring-cahce的另一个配置了!

在缓存过期之后,如果多个线程同时请求对某个数据的访问,会同时去到数据库,导致数据库瞬间负荷增高。Spring4.3为@Cacheable注解提供了一个新的参数“sync”(boolean类型,缺省为false),当设置它为true时,只有一个线程的请求会去到数据库,其他线程都会等待直到缓存可用。这个设置可以减少对数据库的瞬间并发访问。

看到这里!!这不就是一个限流方案么?

所以解决方法就是,加一个属性sync=true,就行。代码就像下面这样

@Cacheable(cacheNames="menu", sync="true")

用了该属性后,可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中。

当然,看到这里,一定会有人和我抬杠!他的问题是这样的!

你这个只是针对单机的限流,并不是整体集群的限流!也就是说,假设你的集群搭建了3000个pod,最差的情况下就是,3000个pod上,每个pod都会发起一个请求去数据库查询,照样还是会导致数据库连接数不够用,等等资源问题!

对于这个问题我只能说!少年,但凡你的公司产品达到这种流量规模,此刻你就不会在看我的文章!你此刻关心的问题是:

(1)哎,买深圳湾一号还是深圳湾公馆呢,纠结! (2)昨天美股又跌了,又损失了两套房 (3)昨天提前撤单了,又少挣了几万 ….(省略一万字)

当然,如果你非要解决,也有办法。springaop有套路的,比如@TransactionalAdviceTransactionInterceptor,那么cache也对应对一个CacheInterceptor,我们只要去改CacheInterceptor,这个切面就能解决。在里头做一个分布式锁!伪代码如下

flag := 取分布式锁
if flag {
走数据库查询,并缓存结果
}{
睡眠一段时间,再次尝试获取key的值
}

但是,我还是要多嘴提一句,真没必要~~记住一句话,立足实际出发~但凡你的业务到了那种级别,是可以做到区域部署的,完全可以规避开这类问题。

缓存雪崩

在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上,如活动系统里面同时进行着非常多的活动,但是在某个时间点所有的活动缓存全部过期。

那么针对该问题,最简单的解决方法就是,过期时间加随机值!

但是很麻烦的是,我们在使用@Cacheable注解的时候,原生功能没法直接设置随机过期时间的。

这个老实说,真没啥好方法,只能自己继承RedisCache,对其增强,改写其中的put方法,带上随机时间!

(本文不赘述,自己可以去查阅相关博客,我真的不喜欢写文章贴大量代码,可读性太差了,知道这么个思路就行,出门搜索一下,一堆答案!)

文末

自此,缓存击穿,穿透,雪崩问题都得到圆满解决~

文章目录
  1. 1. 引言
  2. 2. 正文
    1. 2.1. 目前缺陷
    2. 2.2. 真正方案
    3. 2.3. 缓存穿透
    4. 2.4. 缓存击穿
    5. 2.5. 缓存雪崩
  3. 3. 文末