前言
對於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資料一致性問題的學習和自我發散(胡扯)學習,歡迎進群973961276一起來吹水聊技術,如果有解釋理解不合理或者還請各位大佬指正!
本作品採用《CC 協議》,轉載必須註明作者和本文連結