01 為什么要理解Redis緩存問題?
在高并發(fā)的業(yè)務(wù)場(chǎng)景下奴曙,數(shù)據(jù)庫(kù)大多數(shù)情況都是用戶并發(fā)訪問最薄弱的環(huán)節(jié)。所以坤溃,就需要使用redis做一個(gè)緩沖操作,讓請(qǐng)求先訪問到redis嘱丢,而不是直接訪問Mysql等數(shù)據(jù)庫(kù)薪介。這樣可以大大緩解數(shù)據(jù)庫(kù)的壓力。
當(dāng)緩存庫(kù)出現(xiàn)時(shí)越驻,必須要考慮如下問題:
- 緩存穿透
- 緩存穿擊
- 緩存雪崩
- 緩存污染(或者滿了)
- 緩存和數(shù)據(jù)庫(kù)一致性
02 緩存穿透
- 問題來源
緩存穿透是指緩存和數(shù)據(jù)庫(kù)中都沒有的數(shù)據(jù)汁政,而用戶不斷發(fā)起請(qǐng)求。由于緩存是不命中時(shí)被動(dòng)寫的缀旁,并且出于容錯(cuò)考慮记劈,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢目木,失去了緩存的意義誓禁。
在流量大時(shí)骇两,可能DB就掛掉了示血,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞。
如發(fā)起為id為“-1”的數(shù)據(jù)或id為特別大不存在的數(shù)據(jù)。這時(shí)的用戶很可能是攻擊者烈疚,攻擊會(huì)導(dǎo)致數(shù)據(jù)庫(kù)壓力過大金赦。
- 解決方案
- 接口層增加校驗(yàn),如用戶鑒權(quán)校驗(yàn)珊楼,id做基礎(chǔ)校驗(yàn)已慢,id<=0的直接攔截乍丈;
- 從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫(kù)中也沒有取到亚兄,這時(shí)也可以將key-value對(duì)寫為key-null洽洁,緩存有效時(shí)間可以設(shè)置短點(diǎn),如30秒(設(shè)置太長(zhǎng)會(huì)導(dǎo)致正常情況也沒法使用)。這樣可以防止攻擊用戶反復(fù)用同一個(gè)id暴力攻擊
- 布隆過濾器攒盈。bloomfilter就類似于一個(gè)hash set飘言,用于快速判某個(gè)元素是否存在于集合中句狼,其典型的應(yīng)用場(chǎng)景就是快速判斷一個(gè)key是否存在于某容器,不存在就直接返回铡俐。布隆過濾器的關(guān)鍵就在于hash算法和容器大小,
03 緩存擊穿
- 問題來源
緩存擊穿是指緩存中沒有但數(shù)據(jù)庫(kù)中有的數(shù)據(jù)(一般是緩存時(shí)間到期),這時(shí)由于并發(fā)用戶特別多,同時(shí)讀緩存沒讀到數(shù)據(jù),又同時(shí)去數(shù)據(jù)庫(kù)去取數(shù)據(jù)偎痛,引起數(shù)據(jù)庫(kù)壓力瞬間增大,造成過大壓力娜膘。
- 解決方案
1、設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期甘桑。
2咆耿、接口限流與熔斷吻商,降級(jí)。重要的接口一定要做好限流策略,防止用戶惡意刷接口夹厌,同時(shí)要降級(jí)準(zhǔn)備豹爹,當(dāng)接口中的某些 服務(wù) 不可用時(shí)候,進(jìn)行熔斷矛纹,失敗快速返回機(jī)制臂聋。
3、加互斥鎖
04 緩存雪崩
- 問題來源
緩存雪崩是指緩存中數(shù)據(jù)大批量到過期時(shí)間或南,而查詢數(shù)據(jù)量巨大孩等,引起數(shù)據(jù)庫(kù)壓力過大甚至down機(jī)。和緩存擊穿不同的是采够,緩存擊穿指并發(fā)查同一條數(shù)據(jù)肄方,緩存雪崩是不同數(shù)據(jù)都過期了,很多數(shù)據(jù)都查不到從而查數(shù)據(jù)庫(kù)吁恍。
- 解決方案
- 緩存數(shù)據(jù)的過期時(shí)間設(shè)置隨機(jī)扒秸,防止同一時(shí)間大量數(shù)據(jù)過期現(xiàn)象發(fā)生。
- 如果緩存數(shù)據(jù)庫(kù)是分布式部署冀瓦,將熱點(diǎn)數(shù)據(jù)均勻分布在不同的緩存數(shù)據(jù)庫(kù)中伴奥。
- 設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期。
05 緩存污染(或滿了)
緩存污染問題說的是緩存中一些只會(huì)被訪問一次或者幾次的的數(shù)據(jù)翼闽,被訪問完后拾徙,再也不會(huì)被訪問到,但這部分?jǐn)?shù)據(jù)依然留存在緩存中感局,消耗緩存空間尼啡。
緩存污染會(huì)隨著數(shù)據(jù)的持續(xù)增加而逐漸顯露暂衡,隨著服務(wù)的不斷運(yùn)行,緩存中會(huì)存在大量的永遠(yuǎn)不會(huì)再次被訪問的數(shù)據(jù)崖瞭。緩存空間是有限的狂巢,如果緩存空間滿了,再往緩存里寫數(shù)據(jù)時(shí)就會(huì)有額外開銷书聚,影響Redis性能唧领。這部分額外開銷主要是指寫的時(shí)候判斷淘汰策略,根據(jù)淘汰策略去選擇要淘汰的數(shù)據(jù)雌续,然后進(jìn)行刪除操作斩个。
5.1 最大緩存設(shè)置多大
系統(tǒng)的設(shè)計(jì)選擇是一個(gè)權(quán)衡的過程:大容量緩存是能帶來性能加速的收益,但是成本也會(huì)更高驯杜,而小容量緩存不一定就起不到加速訪問的效果受啥。一般來說,我會(huì)建議把緩存容量設(shè)置為總數(shù)據(jù)量的 15% 到 30%鸽心,兼顧訪問性能和內(nèi)存空間開銷滚局。
對(duì)于 Redis 來說,一旦確定了緩存最大容量再悼,比如 4GB核畴,你就可以使用下面這個(gè)命令來設(shè)定緩存的大小了:
CONFIG SET maxmemory 4gb
@pdai: 代碼已經(jīng)復(fù)制到剪貼板
不過,緩存被寫滿是不可避免的, 所以需要數(shù)據(jù)淘汰策略冲九。
5.2 緩存淘汰策略
Redis共支持八種淘汰策略谤草,分別是noeviction、volatile-random莺奸、volatile-ttl丑孩、volatile-lru、volatile-lfu灭贷、allkeys-lru温学、allkeys-random 和 allkeys-lfu 策略。
怎么理解呢甚疟?主要看分三類看:
不淘汰
noeviction (v4.0后默認(rèn)的)
對(duì)設(shè)置了過期時(shí)間的數(shù)據(jù)中進(jìn)行淘汰
隨機(jī):volatile-random
ttl:volatile-ttl
lru:volatile-lru
lfu:volatile-lfu
全部數(shù)據(jù)進(jìn)行淘汰
隨機(jī):allkeys-random
lru:allkeys-lru
lfu:allkeys-lfu
具體對(duì)照下:
- noeviction
該策略是Redis的默認(rèn)策略仗岖。在這種策略下,一旦緩存被寫滿了览妖,再有寫請(qǐng)求來時(shí)轧拄,Redis 不再提供服務(wù),而是直接返回錯(cuò)誤讽膏。這種策略不會(huì)淘汰數(shù)據(jù)檩电,所以無法解決緩存污染問題。一般生產(chǎn)環(huán)境不建議使用。
其他七種規(guī)則都會(huì)根據(jù)自己相應(yīng)的規(guī)則來選擇數(shù)據(jù)進(jìn)行刪除操作俐末。
- volatile-random
這個(gè)算法比較簡(jiǎn)單料按,在設(shè)置了過期時(shí)間的鍵值對(duì)中,進(jìn)行隨機(jī)刪除卓箫。因?yàn)槭请S機(jī)刪除载矿,無法把不再訪問的數(shù)據(jù)篩選出來,所以可能依然會(huì)存在緩存污染現(xiàn)象烹卒,無法解決緩存污染問題恢准。
- volatile-ttl
這種算法判斷淘汰數(shù)據(jù)時(shí)參考的指標(biāo)比隨即刪除時(shí)多進(jìn)行一步過期時(shí)間的排序。Redis在篩選需刪除的數(shù)據(jù)時(shí)甫题,越早過期的數(shù)據(jù)越優(yōu)先被選擇。
- volatile-lru
LRU算法:LRU 算法的全稱是 Least Recently Used涂召,按照最近最少使用的原則來篩選數(shù)據(jù)坠非。這種模式下會(huì)使用 LRU 算法篩選設(shè)置了過期時(shí)間的鍵值對(duì)。
Redis優(yōu)化的LRU算法實(shí)現(xiàn):
Redis會(huì)記錄每個(gè)數(shù)據(jù)的最近一次被訪問的時(shí)間戳果正。在Redis在決定淘汰的數(shù)據(jù)時(shí)炎码,第一次會(huì)隨機(jī)選出 N 個(gè)數(shù)據(jù),把它們作為一個(gè)候選集合秋泳。接下來潦闲,Redis 會(huì)比較這 N 個(gè)數(shù)據(jù)的 lru 字段,把 lru 字段值最小的數(shù)據(jù)從緩存中淘汰出去迫皱。通過隨機(jī)讀取待刪除集合歉闰,可以讓Redis不用維護(hù)一個(gè)巨大的鏈表,也不用操作鏈表卓起,進(jìn)而提升性能和敬。
Redis 選出的數(shù)據(jù)個(gè)數(shù) N,通過 配置參數(shù) maxmemory-samples 進(jìn)行配置戏阅。個(gè)數(shù)N越大昼弟,則候選集合越大,選擇到的最久未被使用的就更準(zhǔn)確奕筐,N越小舱痘,選擇到最久未被使用的數(shù)據(jù)的概率也會(huì)隨之減小。
- volatile-lfu
會(huì)使用 LFU 算法選擇設(shè)置了過期時(shí)間的鍵值對(duì)离赫。
LFU 算法:LFU 緩存策略是在 LRU 策略基礎(chǔ)上芭逝,為每個(gè)數(shù)據(jù)增加了一個(gè)計(jì)數(shù)器,來統(tǒng)計(jì)這個(gè)數(shù)據(jù)的訪問次數(shù)笆怠。當(dāng)使用 LFU 策略篩選淘汰數(shù)據(jù)時(shí)铝耻,首先會(huì)根據(jù)數(shù)據(jù)的訪問次數(shù)進(jìn)行篩選,把訪問次數(shù)最低的數(shù)據(jù)淘汰出緩存。如果兩個(gè)數(shù)據(jù)的訪問次數(shù)相同瓢捉,LFU 策略再比較這兩個(gè)數(shù)據(jù)的訪問時(shí)效性频丘,把距離上一次訪問時(shí)間更久的數(shù)據(jù)淘汰出緩存。 Redis的LFU算法實(shí)現(xiàn):
當(dāng) LFU 策略篩選數(shù)據(jù)時(shí)泡态,Redis 會(huì)在候選集合中搂漠,根據(jù)數(shù)據(jù) lru 字段的后 8bit 選擇訪問次數(shù)最少的數(shù)據(jù)進(jìn)行淘汰。當(dāng)訪問次數(shù)相同時(shí)某弦,再根據(jù) lru 字段的前 16bit 值大小桐汤,選擇訪問時(shí)間最久遠(yuǎn)的數(shù)據(jù)進(jìn)行淘汰。
Redis 只使用了 8bit 記錄數(shù)據(jù)的訪問次數(shù)靶壮,而 8bit 記錄的最大值是 255怔毛,這樣在訪問快速的情況下,如果每次被訪問就將訪問次數(shù)加一腾降,很快某條數(shù)據(jù)就達(dá)到最大值255拣度,可能很多數(shù)據(jù)都是255,那么退化成LRU算法了螃壤。所以Redis為了解決這個(gè)問題抗果,實(shí)現(xiàn)了一個(gè)更優(yōu)的計(jì)數(shù)規(guī)則,并可以通過配置項(xiàng)奸晴,來控制計(jì)數(shù)器增加的速度冤馏。
參數(shù) :
lfu-log-factor ,用計(jì)數(shù)器當(dāng)前的值乘以配置項(xiàng) lfu_log_factor 再加 1寄啼,再取其倒數(shù)逮光,得到一個(gè) p 值;然后墩划,把這個(gè) p 值和一個(gè)取值范圍在(0睦霎,1)間的隨機(jī)數(shù) r 值比大小,只有 p 值大于 r 值時(shí)走诞,計(jì)數(shù)器才加 1副女。
lfu-decay-time, 控制訪問次數(shù)衰減蚣旱。LFU 策略會(huì)計(jì)算當(dāng)前時(shí)間和數(shù)據(jù)最近一次訪問時(shí)間的差值碑幅,并把這個(gè)差值換算成以分鐘為單位。然后塞绿,LFU 策略再把這個(gè)差值除以 lfu_decay_time 值沟涨,所得的結(jié)果就是數(shù)據(jù) counter 要衰減的值。
lfu-log-factor設(shè)置越大异吻,遞增概率越低裹赴,lfu-decay-time設(shè)置越大喜庞,衰減速度會(huì)越慢。
我們?cè)趹?yīng)用 LFU 策略時(shí)棋返,一般可以將 lfu_log_factor 取值為 10延都。 如果業(yè)務(wù)應(yīng)用中有短時(shí)高頻訪問的數(shù)據(jù)的話,建議把 lfu_decay_time 值設(shè)置為 1睛竣∥浚可以快速衰減訪問次數(shù)。
volatile-lfu 策略是 Redis 4.0 后新增射沟。
- allkeys-lru
使用 LRU 算法在所有數(shù)據(jù)中進(jìn)行篩選殊者。具體LFU算法跟上述 volatile-lru 中介紹的一致,只是篩選的數(shù)據(jù)范圍是全部緩存验夯,這里就不在重復(fù)猖吴。
- allkeys-random
從所有鍵值對(duì)中隨機(jī)選擇并刪除數(shù)據(jù)。volatile-random 跟 allkeys-random算法一樣挥转,隨機(jī)刪除就無法解決緩存污染問題距误。
- allkeys-lfu 使用 LFU 算法在所有數(shù)據(jù)中進(jìn)行篩選。具體LFU算法跟上述 volatile-lfu 中介紹的一致扁位,只是篩選的數(shù)據(jù)范圍是全部緩存,這里就不在重復(fù)趁俊。
allkeys-lfu 策略是 Redis 4.0 后新增域仇。
06 數(shù)據(jù)庫(kù)和緩存一致性
- 問題來源
使用redis做一個(gè)緩沖操作,讓請(qǐng)求先訪問到redis寺擂,而不是直接訪問MySQL等數(shù)據(jù)庫(kù):
讀取緩存步驟一般沒有什么問題暇务,但是一旦涉及到數(shù)據(jù)更新:數(shù)據(jù)庫(kù)和緩存更新,就容易出現(xiàn)緩存(Redis)和數(shù)據(jù)庫(kù)(MySQL)間的數(shù)據(jù)一致性問題怔软。
不管是先寫MySQL數(shù)據(jù)庫(kù)垦细,再刪除Redis緩存;還是先刪除緩存挡逼,再寫庫(kù)括改,都有可能出現(xiàn)數(shù)據(jù)不一致的情況。舉一個(gè)例子:
1.如果刪除了緩存Redis家坎,還沒有來得及寫庫(kù)MySQL嘱能,另一個(gè)線程就來讀取,發(fā)現(xiàn)緩存為空虱疏,則去數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)寫入緩存惹骂,此時(shí)緩存中為臟數(shù)據(jù)。
2.如果先寫了庫(kù)做瞪,在刪除緩存前对粪,寫庫(kù)的線程宕機(jī)了,沒有刪除掉緩存,則也會(huì)出現(xiàn)數(shù)據(jù)不一致情況著拭。
因?yàn)閷懞妥x是并發(fā)的纱扭,沒法保證順序,就會(huì)出現(xiàn)緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致的問題。
6.1 4種相關(guān)模式
更新緩存的的Design Pattern有四種:Cache aside, Read through, Write through, Write behind caching; 我強(qiáng)烈建議你看看這篇茫死,左耳朵耗子的文章:緩存更新的套路 (opens new window)
節(jié)選最最常用的Cache Aside Pattern, 總結(jié)來說就是
- 讀的時(shí)候跪但,先讀緩存,緩存沒有的話峦萎,就讀數(shù)據(jù)庫(kù)屡久,然后取出數(shù)據(jù)后放入緩存,同時(shí)返回響應(yīng)爱榔。
- 更新的時(shí)候被环,先更新數(shù)據(jù)庫(kù),然后再刪除緩存详幽。
其具體邏輯如下:
- 失效:應(yīng)用程序先從cache取數(shù)據(jù)筛欢,沒有得到,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù)唇聘,成功后版姑,放到緩存中。
- 命中:應(yīng)用程序從cache中取數(shù)據(jù)迟郎,取到后返回剥险。
- 更新:先把數(shù)據(jù)存到數(shù)據(jù)庫(kù)中,成功后宪肖,再讓緩存失效表制。
注意,我們的更新是先更新數(shù)據(jù)庫(kù)控乾,成功后么介,讓緩存失效。那么蜕衡,這種方式是否可以沒有文章前面提到過的那個(gè)問題呢壤短?我們可以腦補(bǔ)一下。
一個(gè)是查詢操作慨仿,一個(gè)是更新操作的并發(fā)鸽扁,首先,沒有了刪除cache數(shù)據(jù)的操作了镶骗,而是先更新了數(shù)據(jù)庫(kù)中的數(shù)據(jù)桶现,此時(shí),緩存依然有效鼎姊,所以骡和,并發(fā)的查詢操作拿的是沒有更新的數(shù)據(jù)相赁,但是,更新操作馬上讓緩存的失效了慰于,后續(xù)的查詢操作再把數(shù)據(jù)從數(shù)據(jù)庫(kù)中拉出來钮科。而不會(huì)像文章開頭的那個(gè)邏輯產(chǎn)生的問題,后續(xù)的查詢操作一直都在取老的數(shù)據(jù)婆赠。
這是標(biāo)準(zhǔn)的design pattern绵脯,包括Facebook的論文《Scaling Memcache at Facebook (opens new window)》也使用了這個(gè)策略。為什么不是寫完數(shù)據(jù)庫(kù)后更新緩存休里?你可以看一下Quora上的這個(gè)問答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend? (opens new window)》憋沿,主要是怕兩個(gè)并發(fā)的寫操作導(dǎo)致臟數(shù)據(jù)圾旨。
那么痒谴,是不是Cache Aside這個(gè)就不會(huì)有并發(fā)問題了是整?不是的,比如拭嫁,一個(gè)是讀操作可免,但是沒有命中緩存,然后就到數(shù)據(jù)庫(kù)中取數(shù)據(jù)做粤,此時(shí)來了一個(gè)寫操作浇借,寫完數(shù)據(jù)庫(kù)后,讓緩存失效怕品,然后妇垢,之前的那個(gè)讀操作再把老的數(shù)據(jù)放進(jìn)去,所以堵泽,會(huì)造成臟數(shù)據(jù)。
但恢总,這個(gè)case理論上會(huì)出現(xiàn)迎罗,不過,實(shí)際上出現(xiàn)的概率可能非常低片仿,因?yàn)檫@個(gè)條件需要發(fā)生在讀緩存時(shí)緩存失效纹安,而且并發(fā)著有一個(gè)寫操作。而實(shí)際上數(shù)據(jù)庫(kù)的寫操作會(huì)比讀操作慢得多砂豌,而且還要鎖表厢岂,而讀操作必須在寫操作前進(jìn)入數(shù)據(jù)庫(kù)操作,而又要晚于寫操作更新緩存阳距,所有的這些條件都具備的概率基本并不大塔粒。
所以,這也就是Quora上的那個(gè)答案里說的筐摘,要么通過2PC或是Paxos協(xié)議保證一致性卒茬,要么就是拼命的降低并發(fā)時(shí)臟數(shù)據(jù)的概率船老,而Facebook使用了這個(gè)降低概率的玩法,因?yàn)?PC太慢圃酵,而Paxos太復(fù)雜柳畔。當(dāng)然,最好還是為緩存設(shè)置上過期時(shí)間郭赐。
6.2 方案:隊(duì)列 + 重試機(jī)制
流程如下所示
- 更新數(shù)據(jù)庫(kù)數(shù)據(jù)薪韩;
- 緩存因?yàn)榉N種問題刪除失敗
- 將需要?jiǎng)h除的key發(fā)送至消息隊(duì)列
- 自己消費(fèi)消息,獲得需要?jiǎng)h除的key
- 繼續(xù)重試刪除操作捌锭,直到成功
然而俘陷,該方案有一個(gè)缺點(diǎn),對(duì)業(yè)務(wù)線代碼造成大量的侵入舀锨。于是有了方案二岭洲,在方案二中,啟動(dòng)一個(gè)訂閱程序去訂閱數(shù)據(jù)庫(kù)的binlog坎匿,獲得需要操作的數(shù)據(jù)盾剩。在應(yīng)用程序中,另起一段程序替蔬,獲得這個(gè)訂閱程序傳來的信息告私,進(jìn)行刪除緩存操作。
6.3 方案:異步更新緩存(基于訂閱binlog的同步機(jī)制)
- 技術(shù)整體思路:
MySQL binlog增量訂閱消費(fèi)+消息隊(duì)列+增量數(shù)據(jù)更新到redis
1)讀Redis:熱數(shù)據(jù)基本都在Redis
2)寫MySQL: 增刪改都是操作MySQL
3)更新Redis數(shù)據(jù):MySQ的數(shù)據(jù)操作binlog承桥,來更新到Redis
- Redis更新
1)數(shù)據(jù)操作主要分為兩大塊:
- 一個(gè)是全量(將全部數(shù)據(jù)一次寫入到redis)
- 一個(gè)是增量(實(shí)時(shí)更新)
這里說的是增量,指的是mysql的update驻粟、insert、delate變更數(shù)據(jù)凶异。
2)讀取binlog后分析 蜀撑,利用消息隊(duì)列,推送更新各臺(tái)的redis緩存數(shù)據(jù)。
這樣一旦MySQL中產(chǎn)生了新的寫入剩彬、更新酷麦、刪除等操作,就可以把binlog相關(guān)的消息推送至Redis喉恋,Redis再根據(jù)binlog中的記錄沃饶,對(duì)Redis進(jìn)行更新。
其實(shí)這種機(jī)制轻黑,很類似MySQL的主從備份機(jī)制糊肤,因?yàn)镸ySQL的主備也是通過binlog來實(shí)現(xiàn)的數(shù)據(jù)一致性。
這里可以結(jié)合使用canal(阿里的一款開源框架)氓鄙,通過該框架可以對(duì)MySQL的binlog進(jìn)行訂閱馆揉,而canal正是模仿了mysql的slave數(shù)據(jù)庫(kù)的備份請(qǐng)求,使得Redis的數(shù)據(jù)更新達(dá)到了相同的效果抖拦。
當(dāng)然把介,這里的消息推送工具你也可以采用別的第三方:kafka勤讽、rabbitMQ等來實(shí)現(xiàn)推送更新Redis。