redis緩存介紹以及常見問題淺析
沒緩存的日子

對于web來說,是用戶量和訪問量支持項目技術(shù)的更迭和前進(jìn)。隨著服務(wù)用戶提升??赡軙霈F(xiàn)一下的一些狀況:
- 頁面并發(fā)量和訪問量并不多,mysql
足以支撐自己邏輯業(yè)務(wù)的發(fā)展。那么其實可以不加緩存。最多對靜態(tài)頁面進(jìn)行緩存即可。 - 頁面的并發(fā)量顯著增多,數(shù)據(jù)庫有些壓力,并且有些數(shù)據(jù)更新頻率較低
反復(fù)被查詢或者查詢速度較慢。那么就可以考慮使用緩存技術(shù)優(yōu)化。對高命中的對象存到key-value形式的redis中,那么,如果數(shù)據(jù)被命中,那么可以省經(jīng)效率很低的db。從高效的redis中查找到數(shù)據(jù)。 - 當(dāng)然,可能還會遇到其他問題,你可以需要靜態(tài)頁面本地緩存,cdn加速,甚至負(fù)載均衡這些方法提高系統(tǒng)并發(fā)量。這里就不做介紹。
緩存思想無處不在
我們從一個算法問題開始了解緩存的意義。
問題1:
- 輸入一個數(shù)n(n<20),求
n!;
分析1:
- 單單考慮算法,不考慮數(shù)值越界問題。 當(dāng)然我們知道
n!=n * (n-1) * (n-2) * ... * 1= n * (n-1)!; 那么我們可以用一個遞歸函數(shù)解決問題。
static long jiecheng(int n)
{
if(n==1||n==0)return 1;
else {
return n*jiecheng(n-1);
}
}
這樣每輸入求一次需要執(zhí)行n次。 問題2:
- 輸入t組數(shù)據(jù)(可能成百上千),每組一個x(n<20),求
x!;
分析2:
- 如果使用
遞歸,輸入t組數(shù)據(jù),每個位x,那么每次都要執(zhí)行 [圖片上傳失敗...(image-8bf557-1565769531992)]Xi
當(dāng)Xi過大或者n過大都會造成不小的負(fù)擔(dān)!時間復(fù)雜度為O(n2) - 那么能否換個思想的。沒錯、是
打表(也可以理解位動態(tài)規(guī)劃)。打表常用于ACM算法中,常用于解決多組輸入輸出、圖論搜索結(jié)果、路徑儲存問題。那么,對于這個求階乘。我們只需要申請一個數(shù)組。每個數(shù)據(jù)為前一個數(shù)據(jù)*當(dāng)前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]);
}
}
}
- 時間復(fù)雜度才O(n)。這里的思想就和
緩存思想差不多。先將數(shù)據(jù)在jiecheng[21]數(shù)組中儲存。執(zhí)行一次計算。當(dāng)后面繼續(xù)訪問的時候就相當(dāng)于當(dāng)問靜態(tài)數(shù)組值。為O(1)。就能大大的減少查詢、執(zhí)行成本啦!
緩存的應(yīng)用場景
- 緩存適用于高并發(fā)的場景,提升服務(wù)容量。主要是將從
經(jīng)常被訪問的數(shù)據(jù)或者查詢成本較高從慢的介質(zhì)中存到比較快的介質(zhì)中,比如從硬盤—>內(nèi)存。我們知道大多數(shù)關(guān)系數(shù)據(jù)庫是基于硬盤讀寫的,其效率和資源有限,而redis等非關(guān)系型就是基于內(nèi)存存儲。其效率差別很大。當(dāng)然,緩存也分為本地緩存和服務(wù)端緩存,這里只講redis的服務(wù)端緩存。 - 舉個例子。例如如果一個接口sql查詢
需要2s。你每次查詢都會2s并且加載的時候都會等在,這個長期等待給用戶的體驗是非常糟糕的。而用戶能夠接受的往往是第一次的等待。如果你用了緩存技術(shù)。你第一次查詢放到redis里面。然后數(shù)據(jù)再從redis返回給你。后面當(dāng)你繼續(xù)訪問這個數(shù)據(jù)的時候。查詢到redis中有備份,那么不需要通過db直接能從redis中獲取數(shù)據(jù)。那么,你想想,從一個key value的Nosql中取一個value能要多久呢! - 所以對于像樣的,有點規(guī)模的網(wǎng)站,緩存is
necessary的.redis也是必不可少的。并且服務(wù)端的緩存設(shè)計也是要根據(jù)業(yè)務(wù)有所區(qū)別的。也要防止占用內(nèi)存過大,redis雪崩等問題。
需要注意的問題
- 緩存使用不當(dāng)會帶來很多問題。所以需要對一些細(xì)節(jié)進(jìn)行認(rèn)真考量和設(shè)計。筆者對于分布式的經(jīng)驗并不是很豐富,就相對于筆者的眼中談?wù)劸彺嬖O(shè)計不好會帶來那些問題。
是否用緩存
- 現(xiàn)在不少項目,為了緩存而緩存,然而緩存并不是適合所有場景,比如如果對
數(shù)據(jù)一致性要求極高,又或者數(shù)據(jù)頻繁更改而查詢并不多。有的可以不需要緩存。因為如果使用redis緩存多多少少可能會遇到數(shù)據(jù)一致性問題。那你可以考慮使用redis做成分布式鎖去鎖sql的數(shù)據(jù)。同樣如果頻繁更新數(shù)據(jù),那么redis能起到的作用就僅僅是多了一層中轉(zhuǎn)站。反而浪費資源。使得傳輸過程臃腫。
過期策略選擇
- 大部分場景
不適合緩存一致存在,首先,你的sql數(shù)據(jù)庫的內(nèi)容可能很多就不說了,另外,返回給你的對象如果是完整的pojo對象還好,但是如果是使用不同參數(shù)各種關(guān)聯(lián)查詢出來的結(jié)果那么redis中會儲存太多冷數(shù)據(jù)。占用資源而得不到銷毀。我們學(xué)過操作系統(tǒng)也知道在計算機的緩存實現(xiàn)中有)先進(jìn)先出的算法(FIFO);最近最少使用算法(LRU);最佳淘汰算法(OPT);最少訪問頁面算法(LFR)等磁盤調(diào)度算法。對于web開發(fā)也可以借鑒。根據(jù)時間來的FIFO是最好實現(xiàn)的。因為redis在全局key支持過期策略。 - 而開發(fā)中可能還會遇到
其他問題。比如過期時間的選擇上,如果過久會導(dǎo)致數(shù)據(jù)聚集。而過少可能導(dǎo)致頻繁查詢數(shù)據(jù)庫甚至可能會導(dǎo)致緩存雪崩等問題。 - 所以,過期策略一定要設(shè)置。并且對于
關(guān)鍵key一定要小心謹(jǐn)慎設(shè)計。
數(shù)據(jù)一致性問題★
上面其實提到數(shù)據(jù)一致性問題。如果對一致性要求極高那么不建議使用緩存。下面稍微梳理一下緩存的數(shù)據(jù)。 在redis緩存中經(jīng)常會遇到數(shù)據(jù)一致性問題。對于一個緩存。下面羅列逼仄
讀
read:從redis中讀取,如果redis中沒有,那么就從mysql中獲取更新redis緩存。 該流程圖描述常規(guī)場景。一般沒啥爭議。

寫1:先更新數(shù)據(jù)庫,再更新緩存(普通低并發(fā))

- 更新數(shù)據(jù)庫信息,再更新redis緩存。這是常規(guī)做法,緩存基于數(shù)據(jù)庫,取自數(shù)據(jù)庫。但是其中可能遇到一些問題。例如上述如果更新緩存失敗(宕機等其他狀況),將會使得數(shù)據(jù)庫和redis
數(shù)據(jù)不一致。造成DB新數(shù)據(jù),緩存舊數(shù)據(jù)。
寫2:先刪除緩存,再寫入數(shù)據(jù)庫(低并發(fā)優(yōu)化)

解決的問題
- 這種情況能夠有效避免寫1中防止寫入redis失敗的問題。將緩存刪除進(jìn)行更新。理想是讓下次訪問redis為空去
mysql取得最新值到緩存中。但是這種情況僅限于低并發(fā)的場景中而不適用高并發(fā)場景。
存在的問題
- 寫2雖然能夠看似寫入redis異常的問題??此戚^為好的解決方案但是在高并發(fā)的方案中其實還是有問題的。我們在寫1討論過如果更新庫成功,緩存更新失敗會導(dǎo)致臟數(shù)據(jù)。我們理想是刪除緩存讓下一個線程訪問適合更新緩存。問題是:如果這下一個線程來的太早、太巧了呢?

- 因為多線程你也不知道誰先誰后,誰快誰慢。如上圖所示情況,將會出現(xiàn)redis緩存數(shù)據(jù)和mysql不一致。當(dāng)然你可以對key進(jìn)行
上鎖。但是鎖這種重量級的東西對并發(fā)功能影響太大,能不用鎖就別用!上述情況就高并發(fā)下依然會造成緩存是舊數(shù)據(jù),DB是新數(shù)據(jù)。并且如果緩存沒有過期這個問題會一致存在。
寫3:延時雙刪策略

- 這個就是延時雙刪策略,能過緩解在寫2中在更新mysql過程中有讀的線程進(jìn)入造成redis緩存與mysql數(shù)據(jù)不一致。方法就是
刪除緩存->更新緩存->延時(幾百ms)(可異步)再次刪除緩存。即使在更新緩存途中發(fā)生寫2的問題。造成數(shù)據(jù)不一致,但是延時(具體實間根據(jù)業(yè)務(wù)來,一般幾百ms)再次刪除也能很快的解決不一致。 - 但是就寫的方案其實還是有漏洞的,比如
第二次刪除錯誤、多寫多讀高并發(fā)情況下對mysql訪問的壓力等等。當(dāng)然你可以選擇用mq等消息隊列異步解決。其實實際的解決很難顧及到萬無一失,所以不少大佬在設(shè)計這一環(huán)節(jié)可能會因為一些紕漏會被噴。作為菜菜的筆者在這里就更不獻(xiàn)丑了,策略只是提供大綱,具體設(shè)計還是需要自己團隊實踐和摸索。并且也對一致性的要求級別有所區(qū)別。
寫4:直接操作緩存,定期寫入sql(適合高并發(fā))
- 當(dāng)有
一堆并發(fā)(寫)扔過來的后,前面幾個方案即使使用消息隊列異步通信但也很難給用戶一個舒適的體驗。并且對大規(guī)模操作sql對系統(tǒng)也會造成不小的壓力。所以還有一種方案就是直接操作緩存,將緩存定期寫入sql。因為redis這種非關(guān)系數(shù)據(jù)庫又基于內(nèi)存操作KV相比傳統(tǒng)關(guān)系型要快很多(找值最多多碰撞幾次)。
[圖片上傳失敗...(image-521252-1565770519509)]
- 上面適用于高并發(fā)情況下業(yè)務(wù)設(shè)計,這個時候以redis數(shù)據(jù)為主,mysql數(shù)據(jù)為輔助。定期插入(好像數(shù)據(jù)備份庫一樣)。當(dāng)然,這種高并發(fā)往往會因為業(yè)務(wù)對
讀、寫的順序等等可能有不同要求,可能還要借助消息隊列以及鎖完成針對業(yè)務(wù)上對數(shù)據(jù)和順序可能會因為高并發(fā)、多線程帶來的不確定性和不穩(wěn)定性。提高業(yè)務(wù)可靠性。
總之,越是高并發(fā)、越是對數(shù)據(jù)一致性要求高的方案在數(shù)據(jù)一致性的設(shè)計方案需要考慮和顧及的越復(fù)雜、越多。上述也是筆者針對redis數(shù)據(jù)一致性問題的學(xué)習(xí)和自我發(fā)散(胡扯)學(xué)習(xí)。如果有解釋理解不合理或者還請聯(lián)系告知!
緩存穿透、緩存雪崩和緩存擊穿
如果不了解,可能對這幾個概念都不了解,聽著感覺太高大上,至少筆者剛開始是這么覺得,本文并不是詳細(xì)介紹如何解決和完美解決,更主要的是認(rèn)識和認(rèn)知吧。
redis緩存穿透

理解
- 重在
穿透吧,也就是訪問透過redis直接經(jīng)過mysql,通常是一個不存在的key,在數(shù)據(jù)庫查詢?yōu)?code>null。每次請求落在數(shù)據(jù)庫、并且高并發(fā)。數(shù)據(jù)庫扛不住會掛掉。
解決方案
- 可以將查到的null設(shè)成該key的緩存對象。
- 當(dāng)然,也可以根據(jù)明顯錯誤的key在邏輯層就就行
驗證。 - 同時,你也可以分析用戶行為,是否為故意請求或者爬蟲、攻擊者。針對用戶訪問做限制。
- 其他等等,比如看到其他人用布隆過濾器(超大型hashmap)過濾。
redis緩存雪崩
理解
- 雪崩,就是某東西蜂擁而至的意思,像雪崩一樣。在這里,就是redis緩存集體大規(guī)模集體失效,在高并發(fā)情況下突然使得key大規(guī)模訪問mysql,使得數(shù)據(jù)庫崩掉??梢韵胂笙聡胰丝诶夏昊?。以后那天人集中在70-80歲,就沒人干活了。國家勞動力就造成壓力。

解決方案
- 通常的解決方案是將key的過期時間后面加上一個
隨機數(shù),讓key均勻的失效。 - 考慮用隊列或者鎖讓程序執(zhí)行在壓力范圍之內(nèi),當(dāng)然這種方案可能會影響并發(fā)量。
redis緩存擊穿
理解

- 擊穿和穿透不同,穿透的意思是想法
繞過redis去使得數(shù)據(jù)庫崩掉。而擊穿你可以理解為正面剛擊穿,這種通常為大量并發(fā)對一個key進(jìn)行大規(guī)模的讀寫操作。這個key在緩存失效期間大量請求數(shù)據(jù)庫,對數(shù)據(jù)庫造成太大壓力使得數(shù)據(jù)庫崩掉。就比如在秒殺場景下10000塊錢的mac和100塊的mac這個100塊的那個訂單肯定會被搶到爆。所以緩存擊穿就是針對某個常用key大量請求導(dǎo)致數(shù)據(jù)庫崩潰。
解決方案
- 能夠達(dá)到這種場景的公司其實不多,我也不清楚他們的具體處理方法,但是一個鎖攔截請求總是能防止數(shù)據(jù)庫崩掉吧。
總結(jié)與感悟
其實緩存看起來,理解起來看似簡單然而實際上的設(shè)計方案非常有學(xué)問。在細(xì)節(jié)設(shè)計上還會遇到消息隊列、布隆過濾器、分布式鎖、服務(wù)降級、熔斷、分流這些。在緩存處理上甚至還有緩存預(yù)熱(提前緩存部分熱點數(shù)據(jù)防止剛開始緩存全部命中導(dǎo)致服務(wù)崩掉)等其他熱門名詞和問題這里就不做介紹了。