天天看點

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,那麼每次都要執行

    $\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