“表哥,我被虐了。” 表弟哭丧着脸对我说道。
“怎么滴,连跪?哥带你飞。”
“不是LOL,我这不秋招嘛,那个面试官逮着我问,把我都问蒙了。” 表弟一脸懵逼,怀疑人生。
“问啥了啊,我平时教你的唱跳 rap 都没用上?” 我幸灾乐祸的说道,这小子平时就跟我吹,什么天天考第一,进大厂跟玩儿一样,这不傻了吧。
“一来就问我一堆缓存的问题,什么是缓存啊,什么读写策略,如何保证缓存高可用啊,缓存穿透怎么处理啊,巴拉巴拉一堆,我记不得了都,整得我脑瓜子疼,你说我一个还没毕业的憨憨,至于嘛?” 表弟闷闷不乐,继续说道:“哥,要不教教我?”
“现在知道自己是憨憨了?我都说了叫你改下简历,你丫的一堆精通我看着都懵逼,梁静茹给你的勇气?”
“对对对我是憨憨,憨中之憨。哥教教我吧。” 表弟一脸舔狗样地看着我。
“哎,想喝奶茶了。这脑子啊突然就一片混沌,你刚说啥来着?”
表弟反应也是很快:“我现在就叫外卖,不,我直接骑我的小电驴去买,哥您稍等片刻,我去去就来。”
这小子转身就要出门,我在背后喊住了他。
“老弟啊,别买奈雪的茶里面的芝芝芒芒啊,那太贵了,我怕我喝不惯。”
表弟的嘴角动了动,最后勉强挤出一个笑容说道:“多喝喝就习惯了,我这就去买。”
什么是缓存
“回来了啊,来来来坐坐坐,哥先来给你讲讲什么是缓存。” 拿着芝芝芒芒深吸了一口,真香啊!
首先这个缓存主要是为了协调访问之间的速度差异而存在的一种东西,可以是一个硬件,也可以是一个数据结构。比如我们的内存就是因为磁盘访问太慢了,用内存存储一些磁盘上的数据,便于CPU的读取。
当然再深入还有L1、L2等缓存,也是因为嫌内存慢而产生的。
这种呢是因为纯粹的访问速度慢,还有一种慢,表明看起来是访问慢,实际上的访问的成本高。
什么意思呢?例如一个复杂的计算,如果每次都去算一遍拿结果,就慢了,于是就用缓存把结果存起来,下次就直接拿结果不用重新算了。这就叫空间换时间。
在我们实际工程应用上缓存可以分为三大类:静态缓存、分布式缓存、本地缓存。
静态缓存常指的是前端静态页面,html 啊,js等等,常放在静态服务器上,还能通过 CDN 来缩减响应的时间,提高用户访问速度。
分布式缓存常指的是利用 Redis 、Memcached 等分布式缓存中间件来存放一些较为常用的数据,多个应用共享缓存,不仅可以提高访问速率,也算上在高并发下起到保护脆弱的数据库作用,算是高并发利器了!
本地缓存常指的是应用在同一个进程中的缓存组件,交互之间不会有网络开销,当你的项目还用不上分布式缓存,就存一些简单的变量时候可以用本地缓存来解决。最简单的 HashMap 就能作为本地缓存,或者Ehcache、Guava Cache等。
“老弟???你口水怎么都留下来了??”
“啊?没没没,只是我也想喝啊,这奶茶太贵了,我都没舍得给我自己搞一杯”,表弟目不转睛的盯着我手里的奶茶说道。
“做梦,赶紧的还问啥来着?” 我又深吸了一大口奶茶,真香!
“还有那啥缓存读写策略,我都不明白啥策略,不就去读缓存,没数据就去数据库读,读完了写到缓存中呗,有缓存直接返回就好了,更新数据就先更新数据库,再更新缓存上的数据不就得了,这么简单还跟我整啥策略,他咋不整个孙子兵法呢?” 表弟一脸不忿道。
“我给你整个爱情三十六计不?小年轻,没经历过社会的毒打,今儿我就带你看看没你想的那么简单!” 撩了撩我所剩无几的 “秀发”,戳了下我那厚重的眼镜。 我清了清嗓子:“听好了!”
缓存读写策略
缓存读写策略其实是应对不同场景的。读的策略你说的没错,基本上都是这么个套路,但是写呢就比较讲究了。主要是就有因为分布式缓存数据是共享的,并且缓存和数据库属于两个独立的组件,这么一来数据就容易出问题。
比如你说的先更新数据库,再更新缓存,假如你现在账上有 100 块钱,你妈给你打了 100 块,数据库把你账上的钱变成了 200,然后呢你爸这时候刚好也给你打了 100 块,数据库账上此时变成了 300,你爸的那个请求把缓存更新成了 300,然后你妈的那个请求把缓存更新成了200,然后你账上就变成了200了。
就是因为并发更新,写入顺序不一样导致的。
直接更新缓存还有个问题,例如A请求准备取缓存中的值,此时的值是1,然后+1,此时B请求也打算这样做,拿到的值也是1,这样更新到缓存里面的值就变成2而不是3。
因此还是得靠咱们得到数据库老大哥,它可以保证数据的正确。数据以它为准。这种策略叫cache aside 旁路缓存策略
,也是最常见的策略。
读策略:从缓存中读数据。命中,则直接返回数据。不命中,则从数据库中查,查到数据后,将数据写入到缓存中,并且返回给用户。
写策略:先更新数据库,然后删除缓存(让数据库来保证数据正确,缓存就不更新,咱就做个搬运工,岂不美滋滋)。
“不对不对,先更新数据库,再删除缓存。假如有个A先读缓存,但是此时缓存是没数据,于是去数据库读数据,读到了数据,然后此时有B来更新数据库,然后删除了缓存。此时A再把缓存写回,这不就出问题了吗?表哥,你行不行啊?奶茶退我!” 表弟不乐意了。
“表弟,有点东西啊!你说的没错。其实不论是先删除再更新,还是先更新再删除,只是后删除出错的概率比较低!因为你说的情况需要一个请求先读缓存,然后需要等待另一个请求更新完数据库再删除缓存之后,再写入缓存,这个读缓存要早于数据库写并晚于数据库更新完毕且删除缓存几率很低。”
“并且一般为了解决最终一致性问题都会设置过期时间,避免脏数据一直存在。所以这种很小很小概率的事件,加上一些补偿措施还是可以容忍的。”
“行吧行吧,好像还挺有道理的,还有啥策略?”
“还有write through / read through
,简单的说就是应用只和缓存打交道,由缓存自身来和数据库打交道。也就是说啥并发更新的咱不用管,write through
就是写数据的时候如果缓存有数据则直接写缓存由缓存组件来同步数据到数据库,如果缓存中没数据可以选择和有数据一样的操作,或者直接写数据库。”
“read through
就是从缓存中读,缓存中没数据由缓存组件自己去数据库加载。不过遗憾的是咱们常用分布式缓存都不支持这种,不过本地缓存例如 Guava cache 的 loading Cache 有点 read through
那味儿。”
“最后一个就是 Write back
,这个策略的核心在于写缓存的时候会把缓存标记为脏,然后当缓存需要再次被使用的时候才会刷到后端存储中。当读缓存如果命中直接返回,如果没得话找干净的地儿(数据块)读取数据写入缓存,假设没有干净的地儿,就把脏的缓存块刷到后端存储,然后再写入数据,标记不为脏。”
“发现没 PageCache 就是这么个读写策略,也就是因为刷盘慢呀,所以脏缓存先存着。怎么样老弟,学会了没?” 我又深吸了一口奶茶,发出了丝丝的声音,满足。
“学会了学会了,哥真棒!那还有缓存高可用呢?怎么说?”
“高啥高,今天就到这,这几个策略你先回去好好学,这奶茶都没了,口干了说不了不了,下次一定下次一定。”
“别啊表哥,这奶茶32一杯呢,这才哪到哪啊?”
“赶紧滴,今天已经说了很多干货了,快一遍消化去”,我不耐烦地说道。
“就这?就这?真是有够好笑呢?我要找姨去!” 表弟要是能打得过我,感觉他应该要冲上来了。
“行行行,改天一定和你说........唉唉唉,把拖鞋放下!快放下! 反了你!!!”