沒緩存的日子:
對於web來說,是用戶量和訪問量支持項目技術的更迭和前進。隨着服務用戶提升。可能會出現一下的一些狀況:
- 頁面並發量和訪問量並不多,mysql
足以支撐
自己邏輯業務的發展。那么其實可以不加緩存。最多對靜態頁面進行緩存即可。 - 頁面的並發量顯著增多,數據庫有些壓力,並且有些數據更新頻率較低
反復被查詢
或者查詢速度較慢
。那么就可以考慮使用緩存技術優化。對高命中的對象存到key-value形式的redis中,那么,如果數據被命中,那么可以省經效率很低的db。從高效的redis中查找到數據。 - 當然,可能還會遇到其他問題,你可以需要靜態頁面本地緩存,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,那么每次都要執行
$\sum_{i=0}^t$Xi
當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的內容深度很深,筆者水平有限可能有地方有錯誤還請大佬指出或者交流。當然本文基本為筆者個人理解難免有疏漏。同時寫本文前也閱讀了一些前輩的文章學習(轉來轉去不知道誰是原創就不放鏈接)還請多多指教!
-
如果對
后端、爬蟲、數據結構算法
等感性趣歡迎關注我的個人公眾號交流:bigsai