看完这篇Redis缓存三大问题,保你能和面试官互扯。
日常的開發(fā)中,無不都是使用數(shù)據(jù)庫來進(jìn)行數(shù)據(jù)的存儲(chǔ),由于一般的系統(tǒng)任務(wù)中通常不會(huì)存在高并發(fā)的情況,所以這樣看起來并沒有什么問題。
一旦涉及大數(shù)據(jù)量的需求,如一些商品搶購的情景,或者主頁訪問量瞬間較大的時(shí)候,單一使用數(shù)據(jù)庫來保存數(shù)據(jù)的系統(tǒng)會(huì)因?yàn)?strong>面向磁盤,磁盤讀/寫速度問題有嚴(yán)重的性能弊端,詳細(xì)的磁盤讀寫原理請參考這一片[]。
在這一瞬間成千上萬的請求到來,需要系統(tǒng)在極短的時(shí)間內(nèi)完成成千上萬次的讀/寫操作,這個(gè)時(shí)候往往不是數(shù)據(jù)庫能夠承受的,極其容易造成數(shù)據(jù)庫系統(tǒng)癱瘓,最終導(dǎo)致服務(wù)宕機(jī)的嚴(yán)重生產(chǎn)問題。
為了克服上述的問題,項(xiàng)目通常會(huì)引入NoSQL技術(shù),這是一種基于內(nèi)存的數(shù)據(jù)庫,并且提供一定的持久化功能。
Redis技術(shù)就是NoSQL技術(shù)中的一種。Redis緩存的使用,極大的提升了應(yīng)用程序的性能和效率,特別是數(shù)據(jù)查詢方面。
但同時(shí),它也帶來了一些問題。其中,最要害的問題,就是數(shù)據(jù)的一致性問題,從嚴(yán)格意義上講,這個(gè)問題無解。如果對數(shù)據(jù)的一致性要求很高,那么就不能使用緩存。
另外的一些典型問題就是,緩存穿透、緩存擊穿和緩存雪崩。本篇文章從實(shí)際代碼操作,來提出解決這三個(gè)緩存問題的方案,畢竟Redis的緩存問題是實(shí)際面試中高頻問點(diǎn),理論和實(shí)操要兼得。
緩存穿透
緩存穿透是指查詢一條數(shù)據(jù)庫和緩存都沒有的一條數(shù)據(jù),就會(huì)一直查詢數(shù)據(jù)庫,對數(shù)據(jù)庫的訪問壓力就會(huì)增大,緩存穿透的解決方案,有以下兩種:
緩存空對象:代碼維護(hù)較簡單,但是效果不好。
布隆過濾器:代碼維護(hù)復(fù)雜,效果很好。
緩存空對象
緩存空對象是指當(dāng)一個(gè)請求過來緩存中和數(shù)據(jù)庫中都不存在該請求的數(shù)據(jù),第一次請求就會(huì)跳過緩存進(jìn)行數(shù)據(jù)庫的訪問,并且訪問數(shù)據(jù)庫后返回為空,此時(shí)也將該空對象進(jìn)行緩存。
若是再次進(jìn)行訪問該空對象的時(shí)候,就會(huì)直接擊中緩存,而不是再次數(shù)據(jù)庫,緩存空對象實(shí)現(xiàn)的原理圖如下:
緩存空對象的實(shí)現(xiàn)代碼如下:public?class?UserServiceImpl?{@AutowiredUserDAO?userDAO;@AutowiredRedisCache?redisCache;public?User?findUser(Integer?id)?{Object?object?=?redisCache.get(Integer.toString(id));//?緩存中存在,直接返回if(object?!=?null)?{//?檢驗(yàn)該對象是否為緩存空對象,是則直接返回nullif(object?instanceof?NullValueResultDO)?{return?null;}return?(User)object;}?else?{??//?緩存中不存在,查詢數(shù)據(jù)庫User?user?=?userDAO.getUser(id);//?存入緩存if(user?!=?null)?{redisCache.put(Integer.toString(id),user);}?else?{//?將空對象存進(jìn)緩存redisCache.put(Integer.toString(id),?new?NullValueResultDO());}return?user;}}?????????? }
緩存空對象的實(shí)現(xiàn)代碼很簡單,但是緩存空對象會(huì)帶來比較大的問題,就是緩存中會(huì)存在很多空對象,占用內(nèi)存的空間,浪費(fèi)資源,一個(gè)解決的辦法就是設(shè)置空對象的較短的過期時(shí)間,代碼如下:
//?在緩存的時(shí)候,添加多一個(gè)該空對象的過期時(shí)間60秒 redisCache.put(Integer.toString(id),?new?NullValueResultDO(),60);布隆過濾器
布隆過濾器是一種基于概率的數(shù)據(jù)結(jié)構(gòu),主要用來判斷某個(gè)元素是否在集合內(nèi),它具有運(yùn)行速度快(時(shí)間效率),占用內(nèi)存小的優(yōu)點(diǎn)(空間效率),但是有一定的誤識(shí)別率和刪除困難的問題。它只能告訴你某個(gè)元素一定不在集合內(nèi)或可能在集合內(nèi)。
在計(jì)算機(jī)科學(xué)中有一種思想:空間換時(shí)間,時(shí)間換空間。一般兩者是不可兼得,而布隆過濾器運(yùn)行效率和空間大小都兼得,它是怎么做到的呢?
在布隆過濾器中引用了一個(gè)誤判率的概念,即它可能會(huì)把不屬于這個(gè)集合的元素認(rèn)為可能屬于這個(gè)集合,但是不會(huì)把屬于這個(gè)集合的認(rèn)為不屬于這個(gè)集合,布隆過濾器的特點(diǎn)如下:
一個(gè)非常大的二進(jìn)制位數(shù)組 (數(shù)組里只有0和1)
若干個(gè)哈希函數(shù)
空間效率和查詢效率高
不存在漏報(bào)(False Negative):某個(gè)元素在某個(gè)集合中,肯定能報(bào)出來。
可能存在誤報(bào)(False Positive):某個(gè)元素不在某個(gè)集合中,可能也被爆出來。
不提供刪除方法,代碼維護(hù)困難。
位數(shù)組初始化都為0,它不存元素的具體值,當(dāng)元素經(jīng)過哈希函數(shù)哈希后的值(也就是數(shù)組下標(biāo))對應(yīng)的數(shù)組位置值改為1。
實(shí)際布隆過濾器存儲(chǔ)數(shù)據(jù)和查詢數(shù)據(jù)的原理圖如下:
可能很多讀者看完上面的特點(diǎn)和原理圖,還是看不懂,別急下面通過圖解一步一步的講解布隆過濾器,總而言之一句簡單的話概括就是布隆過濾器是一個(gè)很大二進(jìn)制的位數(shù)組,數(shù)組里面只存0和1。
初始化的布隆過濾器的結(jié)構(gòu)圖如下:
以上只是畫了布隆過濾器的很小很小的一部分,實(shí)際布隆過濾器是非常大的數(shù)組(這里的大是指它的長度大,并不是指它所占的內(nèi)存空間大)。
那么一個(gè)數(shù)據(jù)是怎么存進(jìn)布隆過濾器的呢?
當(dāng)一個(gè)數(shù)據(jù)進(jìn)行存入布隆過濾器的時(shí)候,會(huì)經(jīng)過如干個(gè)哈希函數(shù)進(jìn)行哈希(若是對哈希函數(shù)還不懂的請參考這一片[]),得到對應(yīng)的哈希值作為數(shù)組的下標(biāo),然后將初始化的位數(shù)組對應(yīng)的下標(biāo)的值修改為1,結(jié)果圖如下:
當(dāng)再次進(jìn)行存入第二個(gè)值的時(shí)候,修改后的結(jié)果的原理圖如下:
所以每次存入一個(gè)數(shù)據(jù),就會(huì)哈希函數(shù)的計(jì)算,計(jì)算的結(jié)果就會(huì)作為下標(biāo),在布隆過濾器中有多少個(gè)哈希函數(shù)就會(huì)計(jì)算出多少個(gè)下標(biāo),布隆過濾器插入的流程如下:
將要添加的元素給m個(gè)哈希函數(shù)
得到對應(yīng)于位數(shù)組上的m個(gè)位置
將這m個(gè)位置設(shè)為1
那么為什么會(huì)有誤判率呢?
假設(shè)在我們多次存入值后,在布隆過濾器中存在x、y、z這三個(gè)值,布隆過濾器的存儲(chǔ)結(jié)構(gòu)圖如下所示:
當(dāng)我們要查詢的時(shí)候,比如查詢a這個(gè)數(shù),實(shí)際中a這個(gè)數(shù)是不存在布隆過濾器中的,經(jīng)過2個(gè)哈希函數(shù)計(jì)算后得到a的哈希值分別為2和13,結(jié)構(gòu)原理圖如下:
經(jīng)過查詢后,發(fā)現(xiàn)2和13位置所存儲(chǔ)的值都為1,但是2和13的下標(biāo)分別是x和z經(jīng)過計(jì)算后的下標(biāo)位置的修改,該布隆過濾器中實(shí)際不存在a,那么布隆過濾器就會(huì)誤判改值可能存在,因?yàn)椴悸∵^濾器不存元素值,所以存在誤判率。
那么具體布隆過布隆過濾的判斷的準(zhǔn)確率和一下兩個(gè)因素有關(guān):
布隆過濾器大小:越大,誤判率就越小,所以說布隆過濾器一般長度都是非常大的。
哈希函數(shù)的個(gè)數(shù):哈希函數(shù)的個(gè)數(shù)越多,那么誤判率就越小。
那么為什么不能刪除元素呢?
原因很簡單,因?yàn)閯h除元素后,將對應(yīng)元素的下標(biāo)設(shè)置為零,可能別的元素的下標(biāo)也引用改下標(biāo),這樣別的元素的判斷就會(huì)收到影響,原理圖如下:
當(dāng)你刪除z元素之后,將對應(yīng)的下標(biāo)10和13設(shè)置為0,這樣導(dǎo)致x和y元素的下標(biāo)受到影響,導(dǎo)致數(shù)據(jù)的判斷不準(zhǔn)確,所以直接不提供刪除元素的api。
以上說的都是布隆過濾器的原理,只有理解了原理,在實(shí)際的運(yùn)用才能如魚得水,下面就來實(shí)操代碼,手寫一個(gè)簡單的布隆過濾器。
對于要手寫一個(gè)布隆過濾器,首先要明確布隆過濾器的核心:
若干哈希函數(shù)
存值的Api
判斷值得Api
實(shí)現(xiàn)的代碼如下:
public?class?MyBloomFilter?{//?布隆過濾器長度private?static?final?int?SIZE?=?2?<<?10;//?模擬實(shí)現(xiàn)不同的哈希函數(shù)private?static?final?int[]?num=?new?int[]?{5,?19,?23,?31,47,?71};???//?初始化位數(shù)組private?BitSet?bits?=?new?BitSet(SIZE);//?用于存儲(chǔ)哈希函數(shù)private?MyHash[]?function?=?new?MyHash[num.length];//?初始化哈希函數(shù)public?MyBloomFilter()?{for?(int?i?=?0;?i?<?num.length;?i++)?{function?[i]?=?new?MyHash(SIZE,?num[i]);}}//?存值A(chǔ)pi?public?void?add(String?value)?{//?對存入得值進(jìn)行哈希計(jì)算for?(MyHash?f:?function)?{//?將為數(shù)組對應(yīng)的哈希下標(biāo)得位置得值改為1bits.set(f.hash(value),?true);}}//?判斷是否存在該值得Apipublic?boolean?contains(String?value)?{if?(value?==?null)?{return?false;}boolean?result=?true;for?(MyHash?f?:?func)?{result=?result&&?bits.get(f.hash(value));}return?result;} }哈希函數(shù)代碼如下:
public?static?class?MyHash?{private?int?cap;private?int?seed;//?初始化數(shù)據(jù)public?MyHash(int?cap,?int?seed)?{this.cap?=?cap;this.seed?=?seed;}//?哈希函數(shù)public?int?hash(String?value)?{int?result?=?0;int?len?=?value.length();for?(int?i?=?0;?i?<?len;?i++)?{result?=?seed?*?result?+?value.charAt(i);}return?(cap?-?1)?&?result;}}布隆過濾器測試代碼如下:
????public?static?void?test?{String?value?=?"4243212355312";MyBloomFilter?filter?=?new?MyBloomFilter();System.out.println(filter.contains(value));filter.add(value);System.out.println(filter.contains(value));}以上就是手寫了一個(gè)非常簡單的布隆過濾器,但是實(shí)際項(xiàng)目中可能由牛人或者大公司已經(jīng)幫你寫好的,如谷歌的Google Guava,只需要在項(xiàng)目中引入一下依賴:
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>27.0.1-jre</version> </dependency>實(shí)際項(xiàng)目中具體的操作代碼如下:
public?static?void?MyBloomFilterSysConfig?{@AutowiredOrderMapper?orderMapper//?1.創(chuàng)建布隆過濾器??第二個(gè)參數(shù)為預(yù)期數(shù)據(jù)量10000000,第三個(gè)參數(shù)為錯(cuò)誤率0.00001BloomFilter<CharSequence>?bloomFilter?=??BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")),10000000,?0.00001);//?2.獲取所有的訂單,并將訂單的id放進(jìn)布隆過濾器里面List<Order>?orderList?=?orderMapper.findAll()for?(Order?order;orderList?)?{Long?id?=?order.getId();bloomFilter.put(""?+?id);} }在實(shí)際項(xiàng)目中會(huì)啟動(dòng)一個(gè)系統(tǒng)任務(wù)或者定時(shí)任務(wù),來初始化布隆過濾器,將熱點(diǎn)查詢數(shù)據(jù)的id放進(jìn)布隆過濾器里面,當(dāng)用戶再次請求的時(shí)候,使用布隆過濾器進(jìn)行判斷,改訂單的id是否在布隆過濾器中存在,不存在直接返回null,具體操作代碼:
//?判斷訂單id是否在布隆過濾器中存在 bloomFilter.mightContain(""?+?id)布隆過濾器的缺點(diǎn)就是要維持容器中的數(shù)據(jù),因?yàn)橛唵螖?shù)據(jù)肯定是頻繁變化的,實(shí)時(shí)的要更新布隆過濾器中的數(shù)據(jù)為最新。
緩存擊穿
緩存擊穿是指一個(gè)key非常熱點(diǎn),在不停的扛著大并發(fā),大并發(fā)集中對這一個(gè)點(diǎn)進(jìn)行訪問,當(dāng)這個(gè)key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存,直接請求數(shù)據(jù)庫,瞬間對數(shù)據(jù)庫的訪問壓力增大。
緩存擊穿這里強(qiáng)調(diào)的是并發(fā),造成緩存擊穿的原因有以下兩個(gè):
該數(shù)據(jù)沒有人查詢過 ,第一次就大并發(fā)的訪問。(冷門數(shù)據(jù))
添加到了緩存,reids有設(shè)置數(shù)據(jù)失效的時(shí)間 ,這條數(shù)據(jù)剛好失效,大并發(fā)訪問(熱點(diǎn)數(shù)據(jù))
對于緩存擊穿的解決方案就是加鎖,具體實(shí)現(xiàn)的原理圖如下:
當(dāng)用戶出現(xiàn)大并發(fā)訪問的時(shí)候,在查詢緩存的時(shí)候和查詢數(shù)據(jù)庫的過程加鎖,只能第一個(gè)進(jìn)來的請求進(jìn)行執(zhí)行,當(dāng)?shù)谝粋€(gè)請求把該數(shù)據(jù)放進(jìn)緩存中,接下來的訪問就會(huì)直接集中緩存,防止了緩存擊穿。
業(yè)界比價(jià)普遍的一種做法,即根據(jù)key獲取value值為空時(shí),鎖上,從數(shù)據(jù)庫中l(wèi)oad數(shù)據(jù)后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試。這里要注意,分布式環(huán)境中要使用分布式鎖,單機(jī)的話用普通的鎖(synchronized、Lock)就夠了。
下面以一個(gè)獲取商品庫存的案例進(jìn)行代碼的演示,單機(jī)版的鎖實(shí)現(xiàn)具體實(shí)現(xiàn)的代碼如下:
//?獲取庫存數(shù)量 public?String?getProduceNum(String?key)?{try?{synchronized?(this)?{???//加鎖//?緩存中取數(shù)據(jù),并存入緩存中int?num=?Integer.parseInt(redisTemplate.opsForValue().get(key));if?(num>?0)?{//沒查一次庫存-1redisTemplate.opsForValue().set(key,?(num-?1)?+?"");System.out.println("剩余的庫存為num:"?+?(num-?1));}?else?{System.out.println("庫存為0");}}}?catch?(NumberFormatException?e)?{e.printStackTrace();}?finally?{}return?"OK"; }分布式的鎖實(shí)現(xiàn)具體實(shí)現(xiàn)的代碼如下:
public?String?getProduceNum(String?key)?{//?獲取分布式鎖RLock?lock?=?redissonClient.getLock(key);try?{//?獲取庫存數(shù)int?num=?Integer.parseInt(redisTemplate.opsForValue().get(key));??//?上鎖???????????lock.lock();if?(num>?0)?{//減少庫存,并存入緩存中redisTemplate.opsForValue().set(key,?(num?-?1)?+?"");System.out.println("剩余庫存為num:"?+?(num-?1));}?else?{System.out.println("庫存已經(jīng)為0");}}?catch?(NumberFormatException?e)?{e.printStackTrace();}?finally?{//解鎖lock.unlock();}return?"OK"; }緩存雪崩
緩存雪崩 是指在某一個(gè)時(shí)間段,緩存集中過期失效。此刻無數(shù)的請求直接繞開緩存,直接請求數(shù)據(jù)庫。
造成緩存雪崩的原因,有以下兩種:
reids宕機(jī)
大部分?jǐn)?shù)據(jù)失效
比如天貓雙11,馬上就要到雙11零點(diǎn),很快就會(huì)迎來一波搶購,這波商品在23點(diǎn)集中的放入了緩存,假設(shè)緩存一個(gè)小時(shí),那么到了凌晨24點(diǎn)的時(shí)候,這批商品的緩存就都過期了。
而對這批商品的訪問查詢,都落到了數(shù)據(jù)庫上,對于數(shù)據(jù)庫而言,就會(huì)產(chǎn)生周期性的壓力波峰,對數(shù)據(jù)庫造成壓力,甚至壓垮數(shù)據(jù)庫。
緩存雪崩的原理圖如下,當(dāng)正常的情況下,key沒有大量失效的用戶訪問原理圖如下:
當(dāng)某一時(shí)間點(diǎn),key大量失效,造成的緩存雪崩的原理圖如下:
對于緩存雪崩的解決方案有以下兩種:
搭建高可用的集群,防止單機(jī)的redis宕機(jī)。
設(shè)置不同的過期時(shí)間,防止同一時(shí)間內(nèi)大量的key失效。
針對業(yè)務(wù)系統(tǒng),永遠(yuǎn)都是具體情況具體分析,沒有最好,只有最合適。于緩存其它問題,緩存滿了和數(shù)據(jù)丟失等問題,我們后面繼續(xù)深入的學(xué)習(xí)。最后也提一下三個(gè)詞LRU、RDB、AOF,通常我們采用LRU策略處理溢出,Redis的RDB和AOF持久化策略來保證一定情況下的數(shù)據(jù)安全。
END
來和小伙伴們一起向上生長呀~~~
掃描下方二維碼,添加小詹微信,可領(lǐng)取千元大禮包并申請加入 Python學(xué)習(xí)交流群,群內(nèi)僅供學(xué)術(shù)交流,日常互動(dòng),如果是想發(fā)推文、廣告、砍價(jià)小程序的敬請繞道!一定記得備注「交流學(xué)習(xí)」,我會(huì)盡快通過好友申請哦!
(添加人數(shù)較多,請耐心等待)
(掃碼回復(fù) 1024? 即可領(lǐng)取IT資料包)
總結(jié)
以上是生活随笔為你收集整理的看完这篇Redis缓存三大问题,保你能和面试官互扯。的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 求求你!不要在网上乱拷贝代码了!一段网上
- 下一篇: 国产数据库建模工具,看到界面第一眼,良心