前言
原創公衆號:bigsaijava
對於Web來講,用戶量和訪問量增必定程度上推進項目技術和架構的更迭和進步。可能會有如下的一些情況:redis
- 頁面併發量和訪問量並很少,MySQL
足以支撐
本身邏輯業務的發展。那麼其實能夠不加緩存。最多對靜態頁面進行緩存便可。 - 頁面的併發量顯著增多,數據庫有些壓力,而且有些數據更新頻率較低
反覆被查詢
或者查詢速度較慢
。那麼就能夠考慮使用緩存技術優化。對高命中的對象存到key-value形式的Redis中,那麼,若是數據被命中,那麼能夠不通過效率很低的db。從高效的redis中查找到數據。 - 固然,可能還會遇到其餘問題,你還經過靜態頁面緩存頁面、cdn加速、甚至負載均衡這些方法提升系統併發量。這裏就不作介紹。
緩存思想無處不在
咱們從一個算法問題開始瞭解緩存的意義。算法
問題1:sql
- 輸入一個數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數據一致性問題的學習和自我發散(胡扯)學習。若是有解釋理解不合理或者還請各位大佬指正!
最後,感受不錯的話還請一鍵三連,歡迎關注原創公衆號:「bigsai」,在這裏,不只能學到知識和乾貨,還給你準備了不少進階資料,回覆「bigsai」口令便可領取!