一行代码解决缓存击穿的问题,学好这招,轻松解决

目前缺陷

首先,为什么说目前网上流传的方案,落地性差呢,因为都缺乏一个可以和SpringBoot结合起来的真实场景,基本上都脱离了SpringBoot,只站在Java这个层级去分析。那问题就来了,现在还有只用SpringMvc,却不用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,你们觉得这个做法真的靠谱?

(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)昨天提前撤单了,又少挣了几万
....(省略一万字)

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

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

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

缓存雪崩

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容