自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

緩存穿透、緩存并發(fā)、熱點(diǎn)緩存之最佳招式

移動(dòng)開(kāi)發(fā)
我們?cè)陧?xiàng)目中使用緩存通常都是先檢查緩存中是否存在,如果存在直接返回緩存內(nèi)容,如果不存在就直接查詢(xún)數(shù)據(jù)庫(kù)然后再緩存查詢(xún)結(jié)果返回。這個(gè)時(shí)候如果我們查詢(xún)的某一個(gè)數(shù)據(jù)在緩存中一直不存在,就會(huì)造成每一次請(qǐng)求都查詢(xún)DB,這樣緩存就失去了意義,在流量大時(shí),可能DB就掛掉了。

一、前言

我們?cè)谟镁彺娴臅r(shí)候,不管是Redis或者M(jìn)emcached,基本上會(huì)通用遇到以下三個(gè)問(wèn)題:

  • 緩存穿透
  • 緩存并發(fā)
  • 緩存失效
  • 緩存穿透 
img
img

img

注:

上面三個(gè)圖會(huì)有什么問(wèn)題呢?

我們?cè)陧?xiàng)目中使用緩存通常都是先檢查緩存中是否存在,如果存在直接返回緩存內(nèi)容,如果不存在就直接查詢(xún)數(shù)據(jù)庫(kù)然后再緩存查詢(xún)結(jié)果返回。這個(gè)時(shí)候如果我們查詢(xún)的某一個(gè)數(shù)據(jù)在緩存中一直不存在,就會(huì)造成每一次請(qǐng)求都查詢(xún)DB,這樣緩存就失去了意義,在流量大時(shí),可能DB就掛掉了。

那這種問(wèn)題有什么好辦法解決呢?

要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞。

有一個(gè)比較巧妙的作法是,可以將這個(gè)不存在的key預(yù)先設(shè)定一個(gè)值。

比如,”key” , “&&”。

在返回這個(gè)&&值的時(shí)候,我們的應(yīng)用就可以認(rèn)為這是不存在的key,那我們的應(yīng)用就可以決定是否繼續(xù)等待繼續(xù)訪問(wèn),還是放棄掉這次操作。如果繼續(xù)等待訪問(wèn),過(guò)一個(gè)時(shí)間輪詢(xún)點(diǎn)后,再次請(qǐng)求這個(gè)key,如果取到的值不再是&&,則可以認(rèn)為這時(shí)候key有值了,從而避免了透?jìng)鞯綌?shù)據(jù)庫(kù),從而把大量的類(lèi)似請(qǐng)求擋在了緩存之中。

緩存并發(fā)

有時(shí)候如果網(wǎng)站并發(fā)訪問(wèn)高,一個(gè)緩存如果失效,可能出現(xiàn)多個(gè)進(jìn)程同時(shí)查詢(xún)DB,同時(shí)設(shè)置緩存的情況,如果并發(fā)確實(shí)很大,這也可能造成DB壓力過(guò)大,還有緩存頻繁更新的問(wèn)題。

我現(xiàn)在的想法是對(duì)緩存查詢(xún)加鎖,如果KEY不存在,就加鎖,然后查DB入緩存,然后解鎖;其他進(jìn)程如果發(fā)現(xiàn)有鎖就等待,然后等解鎖后返回?cái)?shù)據(jù)或者進(jìn)入DB查詢(xún)。

這種情況和剛才說(shuō)的預(yù)先設(shè)定值問(wèn)題有些類(lèi)似,只不過(guò)利用鎖的方式,會(huì)造成部分請(qǐng)求等待。

緩存失效

引起這個(gè)問(wèn)題的主要原因還是高并發(fā)的時(shí)候,平時(shí)我們?cè)O(shè)定一個(gè)緩存的過(guò)期時(shí)間時(shí),可能有一些會(huì)設(shè)置1分鐘啊,5分鐘這些,并發(fā)很高時(shí)可能會(huì)出在某一個(gè)時(shí)間同時(shí)生成了很多的緩存,并且過(guò)期時(shí)間都一樣,這個(gè)時(shí)候就可能引發(fā)一當(dāng)過(guò)期時(shí)間到后,這些緩存同時(shí)失效,請(qǐng)求全部轉(zhuǎn)發(fā)到DB,DB可能會(huì)壓力過(guò)重。

那如何解決這些問(wèn)題呢?

其中的一個(gè)簡(jiǎn)單方案就時(shí)講緩存失效時(shí)間分散開(kāi),比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過(guò)期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件。

我們討論的第二個(gè)問(wèn)題時(shí)針對(duì)同一個(gè)緩存,第三個(gè)問(wèn)題時(shí)針對(duì)很多緩存。

總結(jié)來(lái)看:

  • 緩存穿透:查詢(xún)一個(gè)必然不存在的數(shù)據(jù)。比如文章表,查詢(xún)一個(gè)不存在的id,每次都會(huì)訪問(wèn)DB,如果有人惡意破壞,很可能直接對(duì)DB造成影響。
  • 緩存失效:如果緩存集中在一段時(shí)間內(nèi)失效,DB的壓力凸顯。這個(gè)沒(méi)有完美解決辦法,但可以分析用戶(hù)行為,盡量讓失效時(shí)間點(diǎn)均勻分布。

當(dāng)發(fā)生大量的緩存穿透,例如對(duì)某個(gè)失效的緩存的大并發(fā)訪問(wèn)就造成了緩存雪崩。

問(wèn)題匯總

問(wèn)題1:

如何解決DB和緩存一致性問(wèn)題?

答:當(dāng)修改了數(shù)據(jù)庫(kù)后,有沒(méi)有及時(shí)修改緩存。這種問(wèn)題,以前有過(guò)實(shí)踐,修改數(shù)據(jù)庫(kù)成功,而修改緩存失敗的情況,最主要就是緩存服務(wù)器掛了。而因?yàn)榫W(wǎng)絡(luò)問(wèn)題引起的沒(méi)有及時(shí)更新,可以通過(guò)重試機(jī)制來(lái)解決。而緩存服務(wù)器掛了,請(qǐng)求首先自然也就無(wú)法到達(dá),從而直接訪問(wèn)到數(shù)據(jù)庫(kù)。那么我們?cè)谛薷臄?shù)據(jù)庫(kù)后,無(wú)法修改緩存,這時(shí)候可以將這條數(shù)據(jù)放到數(shù)據(jù)庫(kù)中,同時(shí)啟動(dòng)一個(gè)異步任務(wù)定時(shí)去檢測(cè)緩存服務(wù)器是否連接成功,一旦連接成功則從數(shù)據(jù)庫(kù)中按順序取出修改數(shù)據(jù),依次進(jìn)行緩存最新值的修改。

問(wèn)題2:

問(wèn)下緩存穿透那塊!例如,一個(gè)用戶(hù)查詢(xún)文章,通過(guò)ID查詢(xún),按照之前說(shuō)的,是將緩存的KEY預(yù)先設(shè)置一個(gè)值,,如果通過(guò)ID插過(guò)來(lái),發(fā)現(xiàn)是預(yù)先設(shè)定的一個(gè)值,比如說(shuō)是“&&”,那之后的繼續(xù)等待訪問(wèn)是什么意思,這個(gè)ID什么時(shí)候會(huì)真正被附上用戶(hù)所需要的值呢?

答:我剛說(shuō)的主要是咱們常用的后面配置,前臺(tái)獲取的場(chǎng)景。前臺(tái)無(wú)法獲取相應(yīng)的key,則等待,或者放棄。當(dāng)在后臺(tái)配置界面上配置了相關(guān)key和value之后,那么以前的key &&也自然會(huì)被替換掉。你說(shuō)的那種情況,自然也應(yīng)該會(huì)有一個(gè)進(jìn)程會(huì)在某一個(gè)時(shí)刻,在緩存中設(shè)置這個(gè)ID,再有新的請(qǐng)求到達(dá)的時(shí)候,就會(huì)獲取到最新的ID和value。

問(wèn)題3:

其實(shí)用redis的話,那天看到一個(gè)不錯(cuò)的例子,雙key,有一個(gè)當(dāng)時(shí)生成的一個(gè)附屬key來(lái)標(biāo)識(shí)數(shù)據(jù)修改到期時(shí)間,然后快到的時(shí)候去重新加載數(shù)據(jù),如果覺(jué)得key多可以把結(jié)束時(shí)間放到主key中,附屬key起到鎖的功能。

答:這種方案,之前我們實(shí)踐過(guò)。這種方案會(huì)產(chǎn)生雙份數(shù)據(jù),而且需要同時(shí)控制附屬key與key之間的關(guān)系,操作上有一定復(fù)雜度。

問(wèn)題4:

多級(jí)緩存是什么概念呢?

答:多級(jí)緩存就像我今天之前給大家發(fā)的文章里面提到了,將ehcache與redis做二級(jí)緩存,就像我之前寫(xiě)的文章提到過(guò)的。但同樣會(huì)存在一致性問(wèn)題,如果我們需要強(qiáng)一致性的話,緩存與數(shù)據(jù)庫(kù)同步是會(huì)存在時(shí)間差的,所以我們?cè)诰唧w開(kāi)發(fā)的過(guò)程中,一定要根據(jù)場(chǎng)景來(lái)具體分析,二級(jí)緩存更多的解決是,緩存穿透與程序的健壯性,當(dāng)集中式緩存出現(xiàn)問(wèn)題的時(shí)候,我們的應(yīng)用能夠繼續(xù)運(yùn)行。

說(shuō)明:本文中提到的緩存可以理解為Redis。

二、緩存穿透與并發(fā)方案

上文中介紹了關(guān)于緩存穿透、并發(fā)的一些常用思路,但是沒(méi)有明確一些思路的使用場(chǎng)景,下面繼續(xù)深入探討。相信不少朋友之前看過(guò)很多類(lèi)似的文章,但是歸根結(jié)底就是二個(gè)問(wèn)題:

  • 如何解決穿透
  • 如何解決并發(fā)

當(dāng)并發(fā)較高的時(shí)候,其實(shí)我是不建議使用緩存過(guò)期這個(gè)策略的,我更希望緩存一直存在,通過(guò)后臺(tái)系統(tǒng)來(lái)更新緩存系統(tǒng)中的數(shù)據(jù)達(dá)到數(shù)據(jù)的一致性目的,有的朋友可能會(huì)質(zhì)疑,如果緩存系統(tǒng)掛了怎么辦,這樣數(shù)據(jù)庫(kù)更新了但是緩存沒(méi)有更新,沒(méi)有達(dá)到一致性的狀態(tài)。

解決問(wèn)題的思路是:

如果緩存是因?yàn)榫W(wǎng)絡(luò)問(wèn)題沒(méi)有更新成功數(shù)據(jù),那么建議重試幾次,如果依然沒(méi)有更新成功則認(rèn)為緩存系統(tǒng)出錯(cuò)不可用,這時(shí)候客戶(hù)端會(huì)將數(shù)據(jù)的KEY插入到消息系統(tǒng)中,消息系統(tǒng)可以過(guò)濾相同的KEY,只需保證消息系統(tǒng)不存在相同的KEY,當(dāng)緩存系統(tǒng)恢復(fù)可用的時(shí)候,依次從mq中取出KEY值然后從數(shù)據(jù)庫(kù)中讀取最新的數(shù)據(jù)更新緩存。

注意:更新緩存之前,緩存中依然有舊數(shù)據(jù),所以不會(huì)造成緩存穿透。

下圖展示了整個(gè)思路的過(guò)程: 

img

看完上面的方案以后,又會(huì)有不少朋友提出疑問(wèn),如果我是第一次使用緩存或者緩存中暫時(shí)沒(méi)有我需要的數(shù)據(jù),那又該如何處理呢?

解決問(wèn)題的思路:

在這種場(chǎng)景下,客戶(hù)端從緩存中根據(jù)KEY讀取數(shù)據(jù),如果讀到了數(shù)據(jù)則流程結(jié)束,如果沒(méi)有讀到數(shù)據(jù)(可能會(huì)有多個(gè)并發(fā)都沒(méi)有讀到數(shù)據(jù)),這時(shí)候使用緩存系統(tǒng)中的setNX方法設(shè)置一個(gè)值(這種方法類(lèi)似加個(gè)鎖),沒(méi)有設(shè)置成功的請(qǐng)求則sleep一段時(shí)間,設(shè)置成功的請(qǐng)求讀取數(shù)據(jù)庫(kù)獲取值,如果獲取到則更新緩存,流程結(jié)束,之前sleep的請(qǐng)求這時(shí)候喚醒后直接再?gòu)木彺嬷凶x取數(shù)據(jù),此時(shí)流程結(jié)束。

在看完這個(gè)流程后,我想這里面會(huì)有一個(gè)漏洞,如果數(shù)據(jù)庫(kù)中沒(méi)有我們需要的數(shù)據(jù)該怎么處理,如果不處理則請(qǐng)求會(huì)造成死循環(huán),不斷的在緩存和數(shù)據(jù)庫(kù)中查詢(xún),這時(shí)候我們會(huì)沿用我之前文章中的如果沒(méi)有讀到數(shù)據(jù)則往緩存中插入一個(gè)NULL字符串的思路,這樣其他請(qǐng)求直接就可以根據(jù)“NULL”進(jìn)行處理,直到后臺(tái)系統(tǒng)在數(shù)據(jù)庫(kù)成功插入數(shù)據(jù)后同步更新清理NULL數(shù)據(jù)和更新緩存。

流程圖如下所示: 

img

總結(jié):

在實(shí)際工作中,我們往往將上面二個(gè)方案組合使用才能達(dá)到最佳效果,雖然第二種方案也會(huì)造成請(qǐng)求阻塞,但是只是在第一次使用或者緩存暫時(shí)沒(méi)有數(shù)據(jù)的情況下才會(huì)產(chǎn)生,在生產(chǎn)中經(jīng)過(guò)檢驗(yàn)在TPS沒(méi)有上萬(wàn)的情況下是不會(huì)造成問(wèn)題的。

三、熱點(diǎn)緩存解決方案

1、緩存使用背景:

我們拿用戶(hù)中心的一個(gè)案例來(lái)說(shuō)明:

每個(gè)用戶(hù)都會(huì)首先獲取自己的用戶(hù)信息,然后再進(jìn)行其他相關(guān)的操作,有可能會(huì)有如下一些場(chǎng)景情況:

  • 會(huì)有大量相同用戶(hù)重復(fù)訪問(wèn)該項(xiàng)目。
  • 會(huì)有同一用戶(hù)頻繁訪問(wèn)同一模塊。

2、思路解析

因?yàn)橛脩?hù)本身是不固定的而且用戶(hù)數(shù)量也有幾百萬(wàn)尤其上千萬(wàn),我們不可能把所有的用戶(hù)信息全部緩存起來(lái),通過(guò)第一個(gè)場(chǎng)景情況可以看到一些規(guī)律,那就是有大量的相同用戶(hù)重復(fù)訪問(wèn),但是究竟是哪些用戶(hù)重復(fù)訪問(wèn)我們也并不知道。

如果有一個(gè)用戶(hù)頻繁刷新讀取項(xiàng)目,那么對(duì)數(shù)據(jù)庫(kù)本身也會(huì)造成較大壓力,當(dāng)然我們也會(huì)有相關(guān)的保護(hù)機(jī)制來(lái)確實(shí)惡意攻擊,可以從前端控制,也可以有采黑名單等機(jī)制,這里不在贅述。如果用緩存的話,我們又該如何控制同一用戶(hù)繁重讀取用戶(hù)信息呢。

請(qǐng)看下圖: 

img

我們會(huì)通過(guò)緩存系統(tǒng)做一個(gè)排序隊(duì)列,比如1000個(gè)用戶(hù),系統(tǒng)會(huì)根據(jù)用戶(hù)的訪問(wèn)時(shí)間更新用戶(hù)信息的時(shí)間,越是最近訪問(wèn)的用戶(hù)排名越排前,系統(tǒng)會(huì)定期過(guò)濾掉排名最后的200個(gè)用戶(hù),然后再?gòu)臄?shù)據(jù)庫(kù)中隨機(jī)取出200個(gè)用戶(hù)加入隊(duì)列,這樣請(qǐng)求每次到達(dá)的時(shí)候,會(huì)先從隊(duì)列中獲取用戶(hù)信息,如果命中則根據(jù)userId,再?gòu)牧硪粋€(gè)緩存數(shù)據(jù)結(jié)構(gòu)中讀取用戶(hù)信息,如果沒(méi)有命中則說(shuō)明該用戶(hù)請(qǐng)求頻率不高。JAVA偽代碼如下所示:

  1. for (int i = 0; i < times; i++) { 
  2.      user = new ExternalUser(); 
  3.      user.setId(i+""); 
  4.      user.setUpdateTime(new Date(System.currentTimeMillis())); 
  5.      CacheUtil.zadd(sortKey, user.getUpdateTime().getTime(), user.getId()); 
  6.      CacheUtil.putAndThrowError(userKey+user.getId(), JSON.toJSONString(user)); 
  7.  } 
  8.   
  9.  Set<String> userSet = CacheUtil.zrange(sortKey, 0, -1); 
  10.  System.out.println("[sortedSet] - " + JSON.toJSONString(userSet) ); 
  11.  if(userSet == null || userSet.size() == 0) 
  12.      return
  13.   
  14.  Set<Tuple> userSetS = CacheUtil.zrangeWithScores(sortKey, 0, -1); 
  15.  StringBuffer sb = new StringBuffer(); 
  16.  for(Tuple t:userSetS){ 
  17.      sb.append("{member: ").append(t.getElement()).append(", score: ").append(t.getScore()).append("}, "); 
  18.  } 
  19.   
  20.  System.out.println("[sortedcollect] - " + sb.toString().substring(0, sb.length() - 2)); 
  21.   
  22.  Set<String> members = new HashSet<String>(); 
  23.  for(String uid:userSet){ 
  24.      String key = userKey + uid; 
  25.      members.add(uid); 
  26.      ExternalUser user2 = CacheUtil.getObject(key, ExternalUser.class); 
  27.      System.out.println("[user] - " + JSON.toJSONString(user2) ); 
  28.  } 
  29.  System.out.println("[user] - "  + System.currentTimeMillis()); 
  30.   
  31.  String[] keys = new String[members.size()]; 
  32.  members.toArray(keys); 
  33.   
  34.  Long rem = CacheUtil.zrem(sortKey, keys); 
  35.  System.out.println("[rem] - " + rem); 
  36.  userSet = CacheUtil.zrange(sortKey, 0, -1); 
  37.  System.out.println("[remove - sortedSet] - " + JSON.toJSONString(userSet)); 

 

 

責(zé)任編輯:未麗燕 來(lái)源: 程序猿DD
相關(guān)推薦

2023-03-10 13:33:00

緩存穿透緩存擊穿緩存雪崩

2019-10-12 14:19:05

Redis數(shù)據(jù)庫(kù)緩存

2019-11-05 14:24:31

緩存雪崩框架

2021-06-05 09:01:01

Redis緩存雪崩緩存穿透

2018-12-13 12:43:07

Redis緩存穿透

2022-05-27 07:57:20

緩存穿透緩存雪崩緩存擊穿

2022-03-08 00:07:51

緩存雪崩數(shù)據(jù)庫(kù)

2023-11-10 14:58:03

2023-04-14 07:34:19

2024-03-12 10:44:42

2020-10-13 07:44:40

緩存雪崩 穿透

2023-12-06 13:38:00

Redis緩存穿透緩存擊穿

2021-12-25 22:28:27

緩存穿透緩存擊穿緩存雪崩

2022-07-11 07:36:36

緩存緩存雪崩緩存擊穿

2020-03-16 14:57:24

Redis面試雪崩

2020-03-05 09:09:18

緩存原因方案

2020-12-28 12:37:36

緩存擊穿穿透

2023-05-15 10:03:00

Redis緩存穿透

2022-11-18 14:34:28

2023-01-31 08:37:11

緩存穿透擊穿
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)