redis快取介紹與常見問題(精心準備)

  • 2019 年 10 月 7 日
  • 筆記

redis快取介紹與常見問題

寫了挺久的,斷斷續續,正好回家也寫了一點。分享給大家。

文章目錄

  • 沒快取的日子:
  • 快取思想無處不在
  • 快取的應用場景
  • 需要注意的問題
    • 是否用快取
    • 過期策略選擇
    • 數據一致性問題★
      • 寫1:先更新資料庫,再更新快取(普通低並發)
      • 寫2:先刪除快取,再寫入資料庫(低並發優化)
      • 寫3:延時雙刪策略
      • 寫4:直接操作快取,定期寫入sql(適合高並發)
  • 快取穿透、快取雪崩和快取擊穿
    • redis快取穿透
    • redis快取雪崩
    • redis快取擊穿
  • 總結與感悟

沒快取的日子:


對於web來說,是用戶量和訪問量支援項目技術的更迭和前進。隨著服務用戶提升。可能會出現一下的一些狀況:

  1. 頁面並發量和訪問量並不多,mysql足以支撐自己邏輯業務的發展。那麼其實可以不加快取。最多對靜態頁面進行快取即可。
  2. 頁面的並發量顯著增多,資料庫有些壓力,並且有些數據更新頻率較低反覆被查詢或者查詢速度較慢。那麼就可以考慮使用快取技術優化。對高命中的對象存到key-value形式的redis中,那麼,如果數據被命中,那麼可以省經效率很低的db。從高效的redis中查找到數據。
  3. 當然,可能還會遇到其他問題,你可以需要靜態頁面本地快取,cdn加速,甚至負載均衡這些方法提高系統並發量。這裡就不做介紹。

快取思想無處不在


我們從一個演算法問題開始了解快取的意義。

問題1:

  • 輸入一個數n(n<20),求n!

分析1:

  • 單單考慮演算法,不考慮數值越界問題。 當然我們知道n!=n * (n-1) * (n-2) * ... * 1= n * (n-1)!; 那麼我們可以用一個遞歸函數解決問題。
static long jiecheng(int n)  {    if(n==1||n==0)return 1;  else {        return n*jiecheng(n-1);     }  }  

這樣每輸入求一次需要執行n次。 問題2:

  • 輸入t組數據(可能成百上千),每組一個x(n<20),求x!

分析2:

  • 如果使用遞歸,輸入t組數據,每個位x,那麼每次都要執行 當Xi過大或者n過大都會造成不小的負擔!時間複雜度為O(n2)
  • 那麼能否換個思想的。沒錯、是打表(也可以理解位動態規劃)。打表常用於ACM演算法中,常用於解決多組輸入輸出、圖論搜索結果、路徑儲存問題。那麼,對於這個求階乘。我們只需要申請一個數組。每個數據為前一個數據*當前index。那麼思想很明確啦!
import java.util.Scanner;public class test3 {public static void main(String[] args) {  // TODO Auto-generated method stub  Scanner sc=new Scanner(System.in);  int t=sc.nextInt();  long jiecheng[]=new long[21];  jiecheng[0]=1;  for(int i=1;i<21;i++)  {  jiecheng[i]=jiecheng[i-1]*i;  }    for(int i=0;i<t;i++) {  int x=sc.nextInt();  System.out.println(jiecheng[x]);  }}  }
  • 時間複雜度才O(n)。這裡的思想就和快取思想差不多。先將數據在jiecheng[21]數組中儲存。執行一次計算。當後面繼續訪問的時候就相當於當問靜態數組值。為O(1)。就能大大的減少查詢、執行成本啦!

快取的應用場景


  • 快取適用於高並發的場景,提升服務容量。主要是將從經常被訪問的數據或者查詢成本較高從慢的介質中存到比較快的介質中,比如從硬碟—>記憶體。我們知道大多數關係資料庫是基於硬碟讀寫的,其效率和資源有限,而redis等非關係型就是基於記憶體存儲。其效率差別很大。當然,快取也分為本地快取和服務端快取,這裡只講redis的服務端快取。
  • 舉個例子。例如如果一個介面sql查詢需要2s。你每次查詢都會2s並且載入的時候都會等在,這個長期等待給用戶的體驗是非常糟糕的。而用戶能夠接受的往往是第一次的等待。如果你用了快取技術。你第一次查詢放到redis裡面。然後數據再從redis返回給你。後面當你繼續訪問這個數據的時候。查詢到redis中有備份,那麼不需要通過db直接能從redis中獲取數據。那麼,你想想,從一個key value的Nosql中取一個value能要多久呢!
  • 所以對於像樣的,有點規模的網站,快取is necessary的.redis也是必不可少的。並且服務端的快取設計也是要根據業務有所區別的。也要防止佔用記憶體過大,redis雪崩等問題。

需要注意的問題

  • 快取使用不當會帶來很多問題。所以需要對一些細節進行認真考量和設計。筆者對於分散式的經驗並不是很豐富,就相對於筆者的眼中談談快取設計不好會帶來那些問題。

是否用快取

  • 現在不少項目,為了快取而快取,然而快取並不是適合所有場景,比如如果對數據一致性要求極高,又或者數據頻繁更改而查詢並不多。有的可以不需要快取。因為如果使用redis快取多多少少可能會遇到數據一致性問題。那你可以考慮使用redis做成分散式鎖去鎖sql的數據。同樣如果頻繁更新數據,那麼redis能起到的作用就僅僅是多了一層中轉站。反而浪費資源。使得傳輸過程臃腫。

過期策略選擇

  • 大部分場景不適合快取一致存在,首先,你的sql資料庫的內容可能很多就不說了,另外,返回給你的對象如果是完整的pojo對象還好,但是如果是使用不同參數各種關聯查詢出來的結果那麼redis中會儲存太多冷數據。佔用資源而得不到銷毀。我們學過作業系統也知道在電腦的快取實現中有)先進先出的演算法(FIFO);最近最少使用演算法(LRU);最佳淘汰演算法(OPT);最少訪問頁面演算法(LFR)等磁碟調度演算法。對於web開發也可以借鑒。根據時間來的FIFO是最好實現的。因為redis在全局key支援過期策略。
  • 而開發中可能還會遇到其他問題。比如過期時間的選擇上,如果過久會導致數據聚集。而過少可能導致頻繁查詢資料庫甚至可能會導致快取雪崩等問題。
  • 所以,過期策略一定要設置。並且對於關鍵key一定要小心謹慎設計

數據一致性問題★

上面其實提到數據一致性問題。如果對一致性要求極高那麼不建議使用快取。下面稍微梳理一下快取的數據。 在redis快取中經常會遇到數據一致性問題。對於一個快取。下面羅列逼仄

read:從redis中讀取,如果redis中沒有,那麼就從mysql中獲取更新redis快取。 該流程圖描述常規場景。一般沒啥爭議。

寫1:先更新資料庫,再更新快取(普通低並發)

  • 更新資料庫資訊,再更新redis快取。這是常規做法,快取基於資料庫,取自資料庫。但是其中可能遇到一些問題。例如上述如果更新快取失敗(宕機等其他狀況),將會使得資料庫和redis數據不一致。造成DB新數據,快取舊數據。

寫2:先刪除快取,再寫入資料庫(低並發優化)

解決的問題

  • 這種情況能夠有效避免寫1中防止寫入redis失敗的問題。將快取刪除進行更新。理想是讓下次訪問redis為空去mysql取得最新值到快取中。但是這種情況僅限於低並發的場景中而不適用高並發場景。

存在的問題

  • 寫2雖然能夠看似寫入redis異常的問題。看似較為好的解決方案但是在高並發的方案中其實還是有問題的。我們在寫1討論過如果更新庫成功,快取更新失敗會導致臟數據。我們理想是刪除快取讓下一個執行緒訪問適合更新快取。問題是:如果這下一個執行緒來的太早、太巧了呢?
  • 因為多執行緒你也不知道誰先誰後,誰快誰慢。如上圖所示情況,將會出現redis快取數據和mysql不一致。當然你可以對key進行上鎖。但是鎖這種重量級的東西對並發功能影響太大,能不用鎖就別用!上述情況就高並發下依然會造成快取是舊數據,DB是新數據。並且如果快取沒有過期這個問題會一致存在。

寫3:延時雙刪策略

  • 這個就是延時雙刪策略,能過緩解在寫2中在更新mysql過程中有讀的執行緒進入造成redis快取與mysql數據不一致。方法就是刪除快取->更新快取->延時(幾百ms)(可非同步)再次刪除快取。即使在更新快取途中發生寫2的問題。造成數據不一致,但是延時(具體實間根據業務來,一般幾百ms)再次刪除也能很快的解決不一致。
  • 但是就寫的方案其實還是有漏洞的,比如第二次刪除錯誤多寫多讀高並發情況下對mysql訪問的壓力等等。當然你可以選擇用mq等消息隊列非同步解決。其實實際的解決很難顧及到萬無一失,所以不少大佬在設計這一環節可能會因為一些紕漏被噴。作為菜菜的筆者在這裡就更不獻醜了,策略只是提供大綱,具體設計還是需要自己團隊實踐和摸索。並且也對一致性的要求級別有所區別。

寫4:直接操作快取,定期寫入sql(適合高並發)

  • 當有一堆並發(寫)扔過來的後,前面幾個方案即使使用消息隊列非同步通訊但也很難給用戶一個舒適的體驗。並且對大規模操作sql對系統也會造成不小的壓力。所以還有一種方案就是直接操作快取,將快取定期寫入sql。因為redis這種非關係資料庫又基於記憶體操作KV相比傳統關係型要快很多(找值最多多碰撞幾次)。
  • 上面適用於高並發情況下業務設計,這個時候以redis數據為主,mysql數據為輔助。定期插入(好像數據備份庫一樣)。當然,這種高並發往往會因為業務對的順序等等可能有不同要求,可能還要藉助消息隊列以及完成針對業務上對數據和順序可能會因為高並發、多執行緒帶來的不確定性和不穩定性。提高業務可靠性。

總之,越是高並發、越是對數據一致性要求高的方案在數據一致性的設計方案需要考慮和顧及越複雜、越多。上述也是筆者針對redis數據一致性問題的學習和自我發散(胡扯)學習。如果有解釋理解不合理或者還請聯繫告知!

快取穿透、快取雪崩和快取擊穿

如果不了解,可能對這幾個概念都不了解,聽著感覺太高大上,至少筆者剛開始是這麼覺得,本文並不是詳細介紹如何解決和完美解決,更主要的是認識和認知吧。

redis快取穿透

理解

  • 重在穿透吧,也就是訪問透過redis直接經過mysql,通常是一個不存在的key,在資料庫查詢為null。每次請求落在資料庫、並且高並發。資料庫扛不住會掛掉。

解決方案

  • 可以將查到的null設成該key的快取對象。
  • 當然,也可以根據明顯錯誤的key在邏輯層就就行驗證
  • 同時,你也可以分析用戶行為,是否為故意請求或者爬蟲、攻擊者。針對用戶訪問做限制。
  • 其他等等,比如看到其他人用布隆過濾器(超大型hashmap)過濾。

redis快取雪崩

理解

  • 雪崩,就是某東西蜂擁而至的意思,像雪崩一樣。在這裡,就是redis快取集體大規模集體失效,在高並發情況下突然使得key大規模訪問mysql,使得資料庫崩掉。可以想像下國家人口老年化。以後那天人集中在70-80歲,就沒人幹活了。國家勞動力就造成壓力。

解決方案

  • 通常的解決方案是將key的過期時間後面加上一個隨機數,讓key均勻的失效。
  • 考慮用隊列或者鎖讓程式執行在壓力範圍之內,當然這種方案可能會影響並發量。

redis快取擊穿

理解

  • 擊穿和穿透不同,穿透的意思是想法繞過redis去使得資料庫崩掉。而擊穿你可以理解為正面剛擊穿,這種通常為大量並發對一個key進行大規模的讀寫操作。這個key在快取失效期間大量請求資料庫,對資料庫造成太大壓力使得資料庫崩掉。就比如在秒殺場景下10000塊錢的mac和100塊的mac這個100塊的那個訂單肯定會被搶到爆。所以快取擊穿就是針對某個常用key大量請求導致資料庫崩潰。

解決方案

  • 能夠達到這種場景的公司其實不多,我也不清楚他們的具體處理方法,但是一個鎖攔截請求總是能防止資料庫崩掉吧。

總結與感悟

  • 其實快取看起來,理解起來看似簡單然而實際上的設計方案非常有學問。在細節設計上還會遇到消息隊列、布隆過濾器、分散式鎖、服務降級、熔斷、分流這些。在快取處理上甚至還有快取預熱(提前快取部分熱點數據防止剛開始快取全部命中導致服務崩掉)等其他熱門名詞和問題這裡就不做介紹了。
  • 另外在快取設計方面個人感覺和作業系統的存儲管理以及可能遇到的鎖的設計上與讀者優先、寫者優先有著很大關係,大家可以參考和交流!
  • 當然,redis的內容深度很深,筆者水平有限可能有地方有錯誤還請大佬指出或者交流。當然本文基本為筆者個人理解難免有疏漏。同時寫本文前也閱讀了一些前輩的文章學習(轉來轉去不知道誰是原創就不放鏈接)還請多多指教!