前言
首發公眾號:bigsai 頭條號:程序員bigsai 還請關注、一鍵三連!
對于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組數據(可能成百上千),每組一個xi(xi<20),求xi!;
分析2:
- 如果使用遞歸,輸入t組數據,每次輸入為xi,那么每次都要執行次數為: 當每次輸入的Xi過大或者t過大都會造成不小的負擔!時間復雜度為O(n2)
- 那么能否換個思想的。沒錯、是打表。打表常用于ACM算法中,常用于解決多組輸入輸出、圖論搜索結果、路徑儲存問題。那么,對于這個求階乘。我們只需要申請一個數組,按照編號從前往后將在需求的數存到數組中,后面再取得時候直接輸出數組值就可以,思想很明確吧:
import JAVA.util.Scanner;
public class test {
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提高系統吞吐和并發量。
對于常用網站和場景,關系數據庫主要可能放在兩個地方:
- 讀寫IO性能較差
- 一個數據可能通過較大量計算得到
所以使用緩存能夠減少磁盤IO次數和關系數據庫的計算次數。讀取上速度快也從兩個方面體現:
- 基于內存,讀寫較快
- 使用哈希算法直接定位結果不需要計算
所以對于像樣的,有點規模的網站,緩存是很 necessary的,而Redis無疑是最好的選擇之一。
需要注意的問題
緩存使用不當會帶來很多問題。所以需要對一些細節進行認真考量和設計。當然最難得數據一致性在下面單獨分析。
是否用緩存
項目不能為了用緩存而用緩存,緩存并一定適合所有場景,如果對數據一致性要求極高,又或者數據頻繁更改而查詢并不多,又或者根本沒并發量的、查詢簡單的不一定需要緩存,還可能浪費資源使得項目變得臃腫難維護,并且使用redis緩存多多少少可能會遇到數據一致性問題需要考慮。
緩存合理設計
在設計緩存的時候,很可能會遇到多表查詢,如果遇到多表查詢緩存的鍵值對就需要合理考慮,是拆分還是合在一起?當然如果組合種類多但常出現的不多也可以直接緩存,具體的設計要根據項目業務需求來看,并沒有一個非常絕對的標準。
過期策略選擇
- 緩存裝的是相對熱點和常用的數據,Redis資源也是有限,需要選擇一個合理的策略讓緩存過期刪除。我們學過操作系統也知道在計算機的緩存實現中有先進先出的算法(FIFO);最近最少使用算法(LRU);最佳淘汰算法(OPT);最少訪問頁面算法(LFR)等磁盤調度算法。設計Redis緩存時候也可以借鑒。根據時間來的FIFO是最好實現的。且Redis在全局key支持過期策略。
- 并且過期時間也要根據系統情況合理設置,如果硬件好點當前可以稍微久一點,但是過期時間過久或者過短可能都不太好,過短可能緩存命中率不高,而過久很可能造成很多冷門數據存儲在Redis中不釋放。
數據一致性問題★
上面其實提到數據一致性問題。如果對一致性要求極高那么不建議使用緩存。下面稍微梳理一下緩存的數據。 在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數據一致性問題的學習和自我發散(胡扯)學習。如果有解釋理解不合理或者還請各位大佬指正!






