看完這篇Redis緩存三大問題出牧,保你面試能造火箭,工作能擰螺絲

轉(zhuǎn)載:https://www.toutiao.com/a6836181257961865740/

前言

日常的開發(fā)中歇盼,無不都是使用數(shù)據(jù)庫來進行數(shù)據(jù)的存儲舔痕,由于一般的系統(tǒng)任務中通常不會存在高并發(fā)的情況,所以這樣看起來并沒有什么問題豹缀。

一旦涉及大數(shù)據(jù)量的需求伯复,如一些商品搶購的情景,或者主頁訪問量瞬間較大的時候邢笙,單一使用數(shù)據(jù)庫來保存數(shù)據(jù)的系統(tǒng)會因為面向磁盤啸如,磁盤讀/寫速度問題有嚴重的性能弊端。

在這一瞬間成千上萬的請求到來鸣剪,需要系統(tǒng)在極短的時間內(nèi)完成成千上萬次的讀/寫操作组底,這個時候往往不是數(shù)據(jù)庫能夠承受的丈积,極其容易造成數(shù)據(jù)庫系統(tǒng)癱瘓,最終導致服務宕機的嚴重生產(chǎn)問題债鸡。

為了克服上述的問題江滨,項目通常會引入NoSQL技術,這是一種基于內(nèi)存數(shù)據(jù)庫厌均,并且提供一定的持久化功能唬滑。

Redis技術就是NoSQL技術中的一種。Redis緩存的使用棺弊,極大的提升了應用程序的性能和效率晶密,特別是數(shù)據(jù)查詢方面。

但同時模她,它也帶來了一些問題稻艰。其中,最要害的問題侈净,就是數(shù)據(jù)的一致性問題尊勿,從嚴格意義上講,這個問題無解畜侦。如果對數(shù)據(jù)的一致性要求很高元扔,那么就不能使用緩存

另外的一些典型問題就是旋膳,緩存穿透澎语、緩存擊穿緩存雪崩。本篇文章從實際代碼操作验懊,來提出解決這三個緩存問題的方案擅羞,畢竟Redis的緩存問題是實際面試中高頻問點,理論和實操要兼得鲁森。

緩存穿透

緩存穿透是指查詢一條數(shù)據(jù)庫和緩存都沒有的一條數(shù)據(jù)祟滴,就會一直查詢數(shù)據(jù)庫,對數(shù)據(jù)庫的訪問壓力就會增大歌溉,緩存穿透的解決方案垄懂,有以下兩種:

緩存空對象:代碼維護較簡單,但是效果不好痛垛。

布隆過濾器:代碼維護復雜草慧,效果很好。


緩存空對象

緩存空對象是指當一個請求過來緩存中和數(shù)據(jù)庫中都不存在該請求的數(shù)據(jù)匙头,第一次請求就會跳過緩存進行數(shù)據(jù)庫的訪問漫谷,并且訪問數(shù)據(jù)庫后返回為空,此時也將該空對象進行緩存蹂析。

若是再次進行訪問該空對象的時候舔示,就會直接擊中緩存碟婆,而不是再次數(shù)據(jù)庫,緩存空對象實現(xiàn)的原理圖如下:


緩存空對象的實現(xiàn)代碼如下:

publicclassUserServiceImpl{

@Autowired

UserDAOuserDAO;

@Autowired

RedisCache redisCache;

public User findUser(Integer id) {

Object object = redisCache.get(Integer.toString(id));

//緩存中存在惕稻,直接返回

if(object != null) {

//檢驗該對象是否為緩存空對象竖共,是則直接返回null

if(object instanceof NullValueResultDO) {

returnnull;

}

return(User)object;

}else{

//緩存中不存在,查詢數(shù)據(jù)庫

User user = userDAO.getUser(id);

//存入緩存

if(user != null) {

redisCache.put(Integer.toString(id),user);

}else{

//將空對象存進緩存

redisCache.put(Integer.toString(id), new NullValueResultDO());

}

returnuser;

}

}

緩存空對象的實現(xiàn)代碼很簡單俺祠,但是緩存空對象會帶來比較大的問題公给,就是緩存中會存在很多空對象,占用內(nèi)存的空間蜘渣,浪費資源淌铐,一個解決的辦法就是設置空對象的較短的過期時間,代碼如下:

//再緩存的時候蔫缸,添加多一個該空對象的過期時間60秒

redisCache.put(Integer.toString(id),newNullValueResultDO(),60);

布隆過濾器

布隆過濾器是一種基于概率數(shù)據(jù)結構腿准,主要用來判斷某個元素是否在集合內(nèi),它具有運行速度快(時間效率)拾碌,占用內(nèi)存小的優(yōu)點(空間效率)释涛,但是有一定的誤識別率刪除困難的問題。它只能告訴你某個元素一定不在集合內(nèi)或可能在集合內(nèi)倦沧。

在計算機科學中有一種思想:空間換時間,時間換空間它匕。一般兩者是不可兼得展融,而布隆過濾器運行效率和空間大小都兼得,它是怎么做到的呢豫柬?

在布隆過濾器中引用了一個誤判率的概念告希,即它可能會把不屬于這個集合的元素認為可能屬于這個集合,但是不會把屬于這個集合的認為不屬于這個集合烧给,布隆過濾器的特點如下:

一個非常大的二進制位數(shù)組?(數(shù)組里只有0和1)

若干個哈希函數(shù)

空間效率查詢效率高

不存在漏報(False Negative):某個元素在某個集合中燕偶,肯定能報出來。

可能存在誤報(False Positive):某個元素不在某個集合中础嫡,可能也被爆出來指么。

不提供刪除方法,代碼維護困難榴鼎。

位數(shù)組初始化都為0伯诬,它不存元素的具體值,當元素經(jīng)過哈希函數(shù)哈希后的值(也就是數(shù)組下標)對應的數(shù)組位置值改為1巫财。

實際布隆過濾器存儲數(shù)據(jù)和查詢數(shù)據(jù)的原理圖如下:


可能很多讀者看完上面的特點和原理圖盗似,還是看不懂,別急下面通過圖解一步一步的講解布隆過濾器平项,總而言之一句簡單的話概括就是布隆過濾器是一個很大二進制位數(shù)組赫舒,數(shù)組里面只存0和1悍及。

初始化的布隆過濾器的結構圖如下:


以上只是畫了布隆過濾器的很小很小的一部分,實際布隆過濾器是非常大的數(shù)組(這里的大是指它的長度大接癌,并不是指它所占的內(nèi)存空間大)心赶。

那么一個數(shù)據(jù)是怎么存進布隆過濾器的呢?

當一個數(shù)據(jù)進行存入布隆過濾器的時候扔涧,會經(jīng)過若干個哈希函數(shù)進行哈希(若是對哈希函數(shù)還不懂的請參考這一片[])园担,得到對應的哈希值作為數(shù)組的下標,然后將初始化的位數(shù)組對應的下標的值修改為1枯夜,結果圖如下:


當再次進行存入第二個值的時候弯汰,修改后的結果的原理圖如下:


所以每次存入一個數(shù)據(jù),就會哈希函數(shù)的計算湖雹,計算的結果就會作為下標咏闪,在布隆過濾器中有多少個哈希函數(shù)就會計算出多少個下標,布隆過濾器插入的流程如下:

將要添加的元素給m個哈希函數(shù)

得到對應于位數(shù)組上的m個位置

將這m個位置設為1

那么為什么會有誤判率呢摔吏?

假設在我們多次存入值后鸽嫂,在布隆過濾器中存在x、y征讲、z這三個值据某,布隆過濾器的存儲結構圖如下所示:


當我們要查詢的時候,比如查詢a這個數(shù)诗箍,實際中a這個數(shù)是不存在布隆過濾器中的癣籽,經(jīng)過2個哈希函數(shù)計算后得到a的哈希值分別為2和13,結構原理圖如下:


經(jīng)過查詢后滤祖,發(fā)現(xiàn)2和13位置所存儲的值都為1筷狼,但是2和13的下標分別是x和z經(jīng)過計算后的下標位置的修改,該布隆過濾器中實際不存在a匠童,那么布隆過濾器就會誤判該值可能存在埂材,因為布隆過濾器不存元素值,所以存在誤判率汤求。

那么具體布隆過布隆過濾的判斷的準確率和一下兩個因素有關:

布隆過濾器大小:越大俏险,誤判率就越小,所以說布隆過濾器一般長度都是非常大的扬绪。

哈希函數(shù)的個數(shù):哈希函數(shù)的個數(shù)越多寡喝,那么誤判率就越小。

那么為什么不能刪除元素呢勒奇?

原因很簡單预鬓,因為刪除元素后,將對應元素的下標設置為零,可能別的元素的下標也引用改下標格二,這樣別的元素的判斷就會收到影響劈彪,原理圖如下:


當你刪除z元素之后,將對應的下標10和13設置為0顶猜,這樣導致x和y元素的下標受到影響沧奴,導致數(shù)據(jù)的判斷不準確,所以直接不提供刪除元素的api长窄。

以上說的都是布隆過濾器的原理滔吠,只有理解了原理,在實際的運用才能如魚得水挠日,下面就來實操代碼疮绷,手寫一個簡單的布隆過濾器。

對于要手寫一個布隆過濾器嚣潜,首先要明確布隆過濾器的核心:

若干哈希函數(shù)

存值得Api

判斷值得Api

實現(xiàn)的代碼如下:

publicclassMyBloomFilter{

//布隆過濾器長度

privatestaticfinalintSIZE=2<<10;

//模擬實現(xiàn)不同的哈希函數(shù)

privatestaticfinalint[]num=newint[]{5,19,23,31,47,71};

//初始化位數(shù)組

privateBitSetbits=newBitSet(SIZE);

//用于存儲哈希函數(shù)

privateMyHash[]function=newMyHash[num.length];

//初始化哈希函數(shù)

publicMyBloomFilter(){

for(inti=0;i<num.length;i++){

function[i]=newMyHash(SIZE,num[i]);

}

}

//存值Api

publicvoidadd(Stringvalue){

//對存入得值進行哈希計算

for(MyHashf:function){

//將為數(shù)組對應的哈希下標得位置得值改為1

bits.set(f.hash(value),true);

}

}

//判斷是否存在該值得Api

publicbooleancontains(Stringvalue){

if(value==null){

returnfalse;

}

booleanresult=true;

for(MyHashf :func){

result=result&&bits.get(f.hash(value));

}

returnresult;

}

}

哈希函數(shù)代碼如下:

publicstaticclassMyHash{

privateintcap;

privateintseed;

// 初始化數(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;

? ? ? ? }

? ? }

布隆過濾器測試代碼如下:

publicstaticvoidtest {

Stringvalue="4243212355312";

MyBloomFilter filter =newMyBloomFilter();

System.out.println(filter.contains(value));

filter.add(value);

System.out.println(filter.contains(value));

}

以上就是手寫了一個非常簡單得布隆過濾器冬骚,但是實際項目中可能是由牛人或者大公司已經(jīng)幫你寫好的,如谷歌的Google Guava懂算,只需要在項目中引入一下依賴:

com.google.guava

guava

27.0.1-jre

實際項目中具體的操作代碼如下:

publicstaticvoidMyBloomFilterSysConfig {

@Autowired

OrderMapper orderMapper

// 1.創(chuàng)建布隆過濾器? 第二個參數(shù)為預期數(shù)據(jù)量10000000只冻,第三個參數(shù)為錯誤率0.00001

BloomFilter<CharSequence> bloomFilter =? BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")),10000000, 0.00001);

// 2.獲取所有的訂單,并將訂單的id放進布隆過濾器里面

List<Order> orderList = orderMapper.findAll()

for (Order order;orderList ) {

? ? Long id = order.getId();

? ? bloomFilter.put("" + id);

}

}

在實際項目中會啟動一個系統(tǒng)任務或者定時任務计技,來初始化布隆過濾器喜德,將熱點查詢數(shù)據(jù)的id放進布隆過濾器里面,當用戶再次請求的時候垮媒,使用布隆過濾器進行判斷住诸,該訂單的id是否在布隆過濾器中存在,不存在直接返回null涣澡,具體操作代碼:

// 判斷訂單id是否在布隆過濾器中存在

bloomFilter.mightContain(""+ id)

布隆過濾器的缺點就是要維持容器中的數(shù)據(jù),因為訂單數(shù)據(jù)肯定是頻繁變化的丧诺,實時的要更新布隆過濾器中的數(shù)據(jù)為最新入桂。

緩存擊穿

緩存擊穿是指一個key非常熱點,在不停的扛著大并發(fā)驳阎,大并發(fā)集中對這一個點進行訪問抗愁,當這個key在失效的瞬間,持續(xù)的大并發(fā)就穿破緩存呵晚,直接請求數(shù)據(jù)庫蜘腌,瞬間對數(shù)據(jù)庫的訪問壓力增大。

緩存擊穿這里強調(diào)的是并發(fā)饵隙,造成緩存擊穿的原因有以下兩個:

該數(shù)據(jù)沒有人查詢過 撮珠,第一次就大并發(fā)的訪問。(冷門數(shù)據(jù))

添加到了緩存金矛,reids有設置數(shù)據(jù)失效的時間 芯急,這條數(shù)據(jù)剛好失效勺届,大并發(fā)訪問(熱點數(shù)據(jù))

對于緩存擊穿的解決方案就是加鎖,具體實現(xiàn)的原理圖如下:


當用戶出現(xiàn)大并發(fā)訪問的時候娶耍,在查詢緩存的時候和查詢數(shù)據(jù)庫的過程加鎖免姿,只能第一個進來的請求進行執(zhí)行,當?shù)谝粋€請求把該數(shù)據(jù)放進緩存中榕酒,接下來的訪問就會直接集中緩存胚膊,防止了緩存擊穿

業(yè)界比價普遍的一種做法想鹰,即根據(jù)key獲取value值為空時紊婉,鎖上,從數(shù)據(jù)庫中l(wèi)oad數(shù)據(jù)后再釋放鎖杖挣。若其它線程獲取鎖失敗肩榕,則等待一段時間后重試。這里要注意惩妇,分布式環(huán)境中要使用分布式鎖株汉,單機的話用普通的鎖(synchronized、Lock)就夠了歌殃。

下面以一個獲取商品庫存的案例進行代碼的演示乔妈,單機版的鎖實現(xiàn)具體實現(xiàn)的代碼如下:

//獲取庫存數(shù)量

public String getProduceNum(String key) {

try{

synchronized (this) {//加鎖

//緩存中取數(shù)據(jù),并存入緩存中

int num= Integer.parseInt(redisTemplate.opsForValue().get(key));

if(num>0) {

//沒查一次庫存-1

redisTemplate.opsForValue().set(key, (num-1) +"");

System.out.println("剩余的庫存為num:"+ (num-1));

}else{

System.out.println("庫存為0");

}

}

}catch(NumberFormatException e) {

e.printStackTrace();

}finally{

}

return"OK";

}

分布式的鎖實現(xiàn)具體實現(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";

}

緩存雪崩

緩存雪崩 是指在某一個時間段路召,緩存集中過期失效。此刻無數(shù)的請求直接繞開緩存波材,直接請求數(shù)據(jù)庫股淡。

造成緩存雪崩的原因,有以下兩種:

reids宕機

大部分數(shù)據(jù)失效

比如天貓雙11廷区,馬上就要到雙11零點唯灵,很快就會迎來一波搶購,這波商品在23點集中的放入了緩存隙轻,假設緩存一個小時埠帕,那么到了凌晨24點的時候,這批商品的緩存就都過期了玖绿。

而對這批商品的訪問查詢敛瓷,都落到了數(shù)據(jù)庫上,對于數(shù)據(jù)庫而言斑匪,就會產(chǎn)生周期性的壓力波峰呐籽,對數(shù)據(jù)庫造成壓力,甚至壓垮數(shù)據(jù)庫。

緩存雪崩的原理圖如下绝淡,當正常的情況下宙刘,key沒有大量失效的用戶訪問原理圖如下:


當某一時間點,key大量失效牢酵,造成的緩存雪崩的原理圖如下:


對于緩存雪崩的解決方案有以下兩種:

搭建高可用的集群悬包,防止單機的redis宕機。

設置不同的過期時間馍乙,防止同一時間內(nèi)大量的key失效布近。

針對業(yè)務系統(tǒng),永遠都是具體情況具體分析丝格,沒有最好撑瞧,只有最合適。于緩存其它問題显蝌,緩存滿了和數(shù)據(jù)丟失等問題预伺,我們后面繼續(xù)深入的學習。最后也提一下三個詞LRU曼尊、RDB酬诀、AOF,通常我們采用LRU策略處理溢出骆撇,Redis的RDB和AOF持久化策略來保證一定情況下的數(shù)據(jù)安全瞒御。

來源:掘金 鏈接:https://juejin.im/post/5edceb206fb9a047a644684f

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市神郊,隨后出現(xiàn)的幾起案子肴裙,更是在濱河造成了極大的恐慌,老刑警劉巖涌乳,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜻懦,死亡現(xiàn)場離奇詭異,居然都是意外死亡夕晓,警方通過查閱死者的電腦和手機宛乃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來运授,“玉大人,你說我怎么就攤上這事乔煞∮蹼” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵渡贾,是天一觀的道長逗宜。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么纺讲? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任擂仍,我火速辦了婚禮,結果婚禮上熬甚,老公的妹妹穿的比我還像新娘逢渔。我一直安慰自己,他們只是感情好乡括,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布肃廓。 她就那樣靜靜地躺著,像睡著了一般诲泌。 火紅的嫁衣襯著肌膚如雪盲赊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天敷扫,我揣著相機與錄音哀蘑,去河邊找鬼。 笑死葵第,一個胖子當著我的面吹牛绘迁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播羹幸,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼脊髓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了栅受?” 一聲冷哼從身側響起将硝,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎屏镊,沒想到半個月后依疼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡而芥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年律罢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棍丐。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡误辑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出歌逢,到底是詐尸還是另有隱情巾钉,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布秘案,位于F島的核電站砰苍,受9級特大地震影響潦匈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赚导,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一茬缩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吼旧,春花似錦凰锡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厂置,卻和暖如春菩掏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昵济。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工智绸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人访忿。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓瞧栗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親海铆。 傳聞我的和親對象是個殘疾皇子迹恐,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355