3萬字聊聊什么是Redis(六)
大家好,我是Leo
繼上篇Redis技術(shù)總結(jié)五,我們繼續(xù)聊聊Redis的相關(guān)技術(shù)!
上一篇我們介紹了
緩沖區(qū)溢出調(diào)優(yōu)方案。 緩存類型,同步直寫,異步寫回策略。 緩存淘汰策略,LRU,LFU算法的實(shí)現(xiàn)。 臟緩存,干凈緩存的判斷依據(jù)
這篇主要是介紹一下 緩存和數(shù)據(jù)庫不一致,緩存污染,基于SSD實(shí)現(xiàn)大容量,Redis解決大并發(fā),Redis實(shí)現(xiàn)分布式鎖。
推薦閱讀
緩存和數(shù)據(jù)一致性問題
Redis的高頻面試:緩存和數(shù)據(jù)的一致性問題。今天我們整理總結(jié)一下。
Redis的緩存分兩種類型,讀寫緩存,只讀緩存。不同的類型會(huì)產(chǎn)生不同的問題以及不同的解決方案,下面我們了解一下。
讀寫緩存
我們先來介紹一下什么是讀寫緩存。讀寫緩存就代表一個(gè)主庫一樣。既要提供寫服務(wù),也要提供讀服務(wù)。
當(dāng)用戶選用讀寫緩存時(shí),如果對(duì)數(shù)據(jù)發(fā)生了修改。我們除了要考慮數(shù)據(jù)庫的一致性以外,還要考慮緩存中的數(shù)據(jù)一致性。正如我們前面所說,Redis的寫請(qǐng)求也是一個(gè)存儲(chǔ)介質(zhì)。所以我們在配置時(shí),要采取相應(yīng)的寫回策略
當(dāng)采用同步直寫策略,寫緩存時(shí),可以保證緩存與數(shù)據(jù)庫的數(shù)據(jù)一致性。 當(dāng)采用異步寫回策略,寫緩存時(shí),由于是異步執(zhí)行,無法保證命令都執(zhí)行也就是無法保證數(shù)據(jù)一致性。
對(duì)于讀寫緩存來說,如果要保證數(shù)據(jù)一致性就盡量采用同步直寫策略。也就是同步執(zhí)行,程序中一定不要忘記加事務(wù)機(jī)制來保證數(shù)據(jù)庫和緩存的原子性。
有些數(shù)據(jù)要求比較高的可以采用同步直寫,對(duì)要求不高的屬性來說(創(chuàng)建時(shí)間,來源地,家鄉(xiāng),屬性等)我們就可以采用異步寫回策略。
只讀緩存
介紹完讀寫緩存,我相信很多人對(duì)只讀緩存應(yīng)該能知道的差不多了。我再來整理總結(jié)一下。只讀緩存就代表從庫一樣,只負(fù)責(zé)讀服務(wù),不負(fù)責(zé)用戶的寫服務(wù)。
所以平時(shí)我們在?新增數(shù)據(jù)?的時(shí)候就可以繞過Redis,直接寫入數(shù)據(jù)庫,這樣下次用戶訪問的時(shí)候發(fā)現(xiàn)緩存中沒有(緩存缺失)就會(huì)直接打到數(shù)據(jù)庫,在數(shù)據(jù)庫發(fā)現(xiàn)之后就會(huì)緩存下來插入到緩存中。緩存之后我們下次便可以在緩存中直接讀取就不需要再去查詢數(shù)據(jù)庫了。
眾所周知,Redis的并發(fā)承受能力大于MySQL的并發(fā)承受能力。這也是我們?yōu)槭裁匆彺娴絉edis中的原因。
如果是?刪除或修改操作?我們要更新數(shù)據(jù)庫的同時(shí),也要更新緩存。如果不刪除緩存的話就造成了原本這個(gè)用戶不存在,但是可以還是登錄進(jìn)去訪問,就造成了數(shù)據(jù)不一致問題。嚴(yán)重的話可能會(huì)導(dǎo)致系統(tǒng)級(jí)的報(bào)錯(cuò)!(比如關(guān)聯(lián)查詢時(shí),不存在數(shù)據(jù)error)
所以Redis,MySQL都要保證原子性,
先刪緩存,后更新數(shù)據(jù)庫:緩存刪除成功,數(shù)據(jù)更新失敗,導(dǎo)致用戶向緩存讀數(shù)據(jù)時(shí),沒有發(fā)現(xiàn)緩存key就會(huì)打向數(shù)據(jù)庫,而數(shù)據(jù)庫數(shù)據(jù)沒有更新成功導(dǎo)致讀到舊值 先更新數(shù)據(jù)庫,后刪緩存:?更新數(shù)據(jù)庫成功時(shí),緩存刪除失敗,就會(huì)導(dǎo)致數(shù)據(jù)庫保留了最新的值,用戶向Redis讀數(shù)據(jù)時(shí),發(fā)現(xiàn)緩存key存在,直接返回了就拿到了上一個(gè)舊值。
如何解決
1. 重試機(jī)制
我們可以采用消息隊(duì)列的思想去改進(jìn)。如果一條命令執(zhí)行成功,另一條命令執(zhí)行失敗時(shí),寫入消息隊(duì)列,進(jìn)入二次消費(fèi)。當(dāng)消費(fèi)成功時(shí),我們再把消息隊(duì)列的數(shù)據(jù)自行刪除。以免重復(fù)操作!
這樣就保證了數(shù)據(jù)庫和緩存的一致性了!
這種情況的確可以解決數(shù)據(jù)的一致性,但是如果這個(gè)數(shù)據(jù)被并發(fā)訪問的話,失敗的那一刻就造成了舊值的產(chǎn)生!我們再來分析一下
先刪緩存,后更新數(shù)據(jù)庫
假設(shè)緩存刪除成功之后,還沒進(jìn)行數(shù)據(jù)庫的更新操作,這時(shí)有個(gè)用戶請(qǐng)求打了過來。它發(fā)現(xiàn)緩存中不存在這個(gè)數(shù)據(jù),就會(huì)打到數(shù)據(jù)庫。它從數(shù)據(jù)庫中取到了數(shù)據(jù)。這就造成了讀到了舊值。而且讀到舊值的同時(shí)還會(huì)把舊值緩存到Redis中。
隨后數(shù)據(jù)庫的更新操作開始進(jìn)行了。。。。
緩存的值出現(xiàn)了,數(shù)據(jù)庫的值又沒了。。。。
這兩者不就不一樣了嘛!
我們在解決時(shí),一般都會(huì)讓他先sleep一小段時(shí)間,再進(jìn)行緩存刪除操作。sleep的睡眠參數(shù)取決于(線程讀數(shù)據(jù)和寫緩存的操作時(shí)間作為估值)
先更新數(shù)據(jù)庫,后刪緩存
如果數(shù)據(jù)庫的值修改了,刪除緩存的那一刻并發(fā)來了,用戶就會(huì)從緩存匯中讀取舊值直接返回給用戶。
一擊要害!。。。。。
刪除緩存值或更新數(shù)據(jù)庫失敗而導(dǎo)致數(shù)據(jù)不一致,你可以使用重試機(jī)制確保刪除或更新操作成功。
在刪除緩存值、更新數(shù)據(jù)庫的這兩步操作中,有其他線程的并發(fā)讀操作,導(dǎo)致其他線程讀取到舊值,應(yīng)對(duì)方案是延遲雙刪。
緩存雪崩,擊穿,穿透
雪崩
雪崩這個(gè)東西,見名思意。我們可以理解成一大堆雪沖了過來,一面墻無法抵擋猛烈的攻擊。于是沖垮了墻,直接沖向了你們家的臥室!
一般造成緩存雪崩主要是有如下幾個(gè)原因
發(fā)大洪水:系統(tǒng)設(shè)計(jì)問題,明明有100萬流量,程序只設(shè)計(jì)了10萬 洪水從天而降的意外:緩存key剛好大面積失效,過期時(shí)間設(shè)計(jì)的不合理 被瓦匠工糊弄了一下墻倒了:Redis實(shí)例宕機(jī)
我們在平時(shí)解決時(shí),可以避免緩存寫入的時(shí)間大面積相同,可以在后面加一個(gè)隨機(jī)函數(shù),讓過期時(shí)間分布的頻段多一些。還可以通過服務(wù)降級(jí)來解決緩存雪崩。
比如在去年新冠疫情的那會(huì)。嚴(yán)查所有過往的路人,一旦有咳嗽,發(fā)燒一律不予通行。如果沒有咳嗽,發(fā)燒等情況還持有體檢報(bào)告的可以回家自行隔離。
在Redis中也是同樣道理,如果訪問的是核心數(shù)據(jù),我們可以放行,如果是訪問附加屬性我們可以直接返回初始數(shù)據(jù),或者網(wǎng)絡(luò)波動(dòng)問題。這樣就可以過濾一部分附加屬性的請(qǐng)求了。
還有一種情況就是,洪水還沒來,墻自己倒了。你看這不是赤裸裸的求干嗎,你這不就是挑釁洪水的嘛。
一般為了系統(tǒng)業(yè)務(wù)能正常運(yùn)行,我們會(huì)提前最好做好如下應(yīng)對(duì)措施
實(shí)現(xiàn)服務(wù)熔斷 實(shí)現(xiàn)請(qǐng)求限流機(jī)制。 高可用集群
服務(wù)熔斷的話我們可以理解成,為了防止引發(fā)連鎖反應(yīng)(積分服務(wù)掛了,還能影響訂單嘛)我們關(guān)掉了用戶的積分服務(wù)。等修復(fù)成功之后再重新開啟服務(wù)。這樣就可以避免其他服務(wù)受此牽連。
在業(yè)務(wù)系統(tǒng)運(yùn)行時(shí),我們可以監(jiān)測 Redis 緩存所在機(jī)器和數(shù)據(jù)庫所在機(jī)器的負(fù)載指標(biāo),例如每秒請(qǐng)求數(shù)、CPU 利用率、內(nèi)存利用率等。如果我們發(fā)現(xiàn) Redis 緩存實(shí)例宕機(jī)了,而數(shù)據(jù)庫所在機(jī)器的負(fù)載壓力突然增加(例如每秒請(qǐng)求數(shù)激增),此時(shí),就發(fā)生緩存雪崩了。大量請(qǐng)求被發(fā)送到數(shù)據(jù)庫進(jìn)行處理。我們可以啟動(dòng)服務(wù)熔斷機(jī)制,暫停業(yè)務(wù)應(yīng)用對(duì)緩存服務(wù)的訪問,從而降低對(duì)數(shù)據(jù)庫的訪問壓力
服務(wù)限流的話,我們可以理解成早晨上班的警察道路調(diào)配。一個(gè)路口是流入量是不變的,如果我們想正常運(yùn)行,就必須把流入速率慢下來。
回到系統(tǒng)的話就是每秒1萬個(gè)請(qǐng)求,限流之后,每秒1千個(gè)請(qǐng)求。再多的請(qǐng)求我們拒之門外排隊(duì)等候。
高可用集群?的話也算是提前預(yù)防了。就好比在雙十一或者流量超級(jí)春運(yùn)的時(shí)候。流量超級(jí)大。我們提前在節(jié)日之前把對(duì)應(yīng)的機(jī)器設(shè)施架設(shè)起來。一旦大屏面板監(jiān)測到大批流量引入我們可以開啟備選方案,通過增加機(jī)器來解決并發(fā)需求。這樣也可以達(dá)到節(jié)省硬件成本的需求。
擊穿
緩存擊穿主要就是?熱點(diǎn)數(shù)據(jù)失效?。雙十一期間如果榜一的商品緩存失效了,恐怕就有悲劇了。一時(shí)間所有的請(qǐng)求都打到的數(shù)據(jù)庫上。這是由于熱點(diǎn)數(shù)據(jù)的過期時(shí)間設(shè)計(jì)不符導(dǎo)致的。
我們一般對(duì)這類數(shù)據(jù)會(huì)進(jìn)行提前預(yù)熱,比如熱榜前100的數(shù)據(jù),我們會(huì)預(yù)熱30分鐘。這樣在秒殺的時(shí)候,就不會(huì)造成在短時(shí)間內(nèi)大量請(qǐng)求打入數(shù)據(jù)庫了。
穿透
緩存穿透顧名思義,就是在玩刺劍時(shí),攢足力氣,直沖一處。
回到系統(tǒng)中是這樣的意思。黑客?在黑入我們系統(tǒng)時(shí),往往會(huì)猜想一些緩存中沒有的數(shù)據(jù),使大量請(qǐng)求打到數(shù)據(jù)庫,造成緩存穿透。當(dāng)下次再次請(qǐng)求時(shí),緩存中還是沒有查詢到,因?yàn)閺臄?shù)據(jù)庫查詢時(shí),本來就沒有所以也無法寫入緩存。
我們的解決方案就是:不管數(shù)據(jù)庫是否存在當(dāng)前數(shù)據(jù),我們都緩存的一個(gè)key,給這個(gè)key的value中 寫入一個(gè)null。
還可以利用Redis的布隆過濾器來解決。下面我們來聊一下什么是布隆過濾器
布隆過濾器
布隆過濾器由一個(gè)初值都為 0 的 bit 數(shù)組和 N 個(gè)哈希函數(shù)組成,可以用來快速判斷某個(gè)數(shù)據(jù)是否存在。當(dāng)我們想標(biāo)記某個(gè)數(shù)據(jù)存在時(shí)(例如,數(shù)據(jù)已被寫入數(shù)據(jù)庫),布隆過濾器會(huì)通過三個(gè)操作完成標(biāo)記:
首先,使用 N 個(gè)哈希函數(shù),分別計(jì)算這個(gè)數(shù)據(jù)的哈希值,得到 N 個(gè)哈希值。 然后,我們把這 N 個(gè)哈希值對(duì) bit 數(shù)組的長度取模,得到每個(gè)哈希值在數(shù)組中的對(duì)應(yīng)位置。 最后,我們把對(duì)應(yīng)位置的 bit 位設(shè)置為 1,這就完成了在布隆過濾器中標(biāo)記數(shù)據(jù)的操作。
如果數(shù)據(jù)不存在(例如,數(shù)據(jù)庫里沒有寫入數(shù)據(jù)),我們也就沒有用布隆過濾器標(biāo)記過數(shù)據(jù),那么,bit 數(shù)組對(duì)應(yīng) bit 位的值仍然為 0。
當(dāng)需要查詢某個(gè)數(shù)據(jù)時(shí),我們就執(zhí)行剛剛說的計(jì)算過程,先得到這個(gè)數(shù)據(jù)在 bit 數(shù)組中對(duì)應(yīng)的 N 個(gè)位置。緊接著,我們查看 bit 數(shù)組中這 N 個(gè)位置上的 bit 值。只要這 N 個(gè) bit 值有一個(gè)不為 1,這就表明布隆過濾器沒有對(duì)該數(shù)據(jù)做過標(biāo)記,所以,查詢的數(shù)據(jù)一定沒有在數(shù)據(jù)庫中保存。
緩存污染
什么是緩存污染呢?
在一些場景下,有些數(shù)據(jù)被訪問的次數(shù)非常少,甚至只會(huì)被訪問一次。當(dāng)這些數(shù)據(jù)服務(wù)完訪問請(qǐng)求后,如果還繼續(xù)留存在緩存中的話,就只會(huì)白白占用緩存空間。這種情況,就是緩存污染。
如何解決?
原本的計(jì)劃我是想把8種淘汰策略全部聊一遍的,但是看了下時(shí)間3點(diǎn)了。芭比Q了WC。
速戰(zhàn)速?zèng)Q吧,通過前面的幾篇文章我們介紹了詳細(xì)的8種策略。這里的話我就直接省略了。
noeviction策略不進(jìn)行數(shù)據(jù)淘汰所以不能用于緩存污染 volatile-random與allkeys-random都是隨機(jī)淘汰,雖然可以用于淘汰數(shù)據(jù)但是不好,而且如果淘汰了熱點(diǎn)數(shù)據(jù)反而適得其反。 volatile-ttl屬于按照時(shí)間淘汰,和隨機(jī)淘汰一樣,用于解決緩存污染,不是很好,也會(huì)造成適得其反。
剩下的四個(gè)volatile-lru、volatile-lfu、allkeys-lru、allkeys-lfu策略。其實(shí)我們可以看成2個(gè)策略,只不過一個(gè)是局部的一個(gè)是全局的。為了理解我們下面就用?LRU?和?LFU?算法表示。
LRU
廢話不多說,省略一些,想看詳細(xì)的可以去(3萬聊聊什么是Redis五)。LRU算法是比較好的,但是唯一的缺點(diǎn)就是?受訪問時(shí)間影響,因?yàn)橹豢磾?shù)據(jù)的訪問時(shí)間,使用 LRU 策略在處理掃描式單次查詢操作時(shí),無法解決緩存污染。所謂的掃描式單次查詢操作,就是指應(yīng)用對(duì)大量的數(shù)據(jù)進(jìn)行一次全體讀取,每個(gè)數(shù)據(jù)都會(huì)被讀取,而且只會(huì)被讀取一次。此時(shí),因?yàn)檫@些被查詢的數(shù)據(jù)剛剛被訪問過,所以 lru 字段值都很大。
LRU會(huì)把這些數(shù)據(jù)在留在緩存中很長時(shí),造成緩存污染。如果有新數(shù)據(jù)訪問時(shí),還要把舊數(shù)據(jù)替換出去,換新的值進(jìn)來,這樣會(huì)影響緩存的性能。
LFU
在LRU的基礎(chǔ)上,誕生了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ù)淘汰出緩存。
和那些被頻繁訪問的數(shù)據(jù)相比,掃描式單次查詢的數(shù)據(jù)因?yàn)椴粫?huì)被再次訪問,所以它們的訪問次數(shù)不會(huì)再增加。因此,LFU 策略會(huì)優(yōu)先把這些訪問次數(shù)低的數(shù)據(jù)淘汰出緩存。這樣一來,LFU 策略就可以避免這些數(shù)據(jù)對(duì)緩存造成污染了。
LFU為了造成鏈表的開銷,使用了兩個(gè)近似的方案
都可以RedisObject 保存數(shù)據(jù),在結(jié)構(gòu)內(nèi)設(shè)置了一個(gè)lru字段記錄數(shù)據(jù)的時(shí)間戳 采用隨機(jī)采樣的方式選取一定數(shù)據(jù)量放入候選集合,后續(xù)在候選集合中根據(jù)lru字段值的大小進(jìn)行篩選。
Redis 在實(shí)現(xiàn) LFU 策略的時(shí)候,只是把原來 24bit 大小的 lru 字段,又進(jìn)一步拆分成了兩部分。
ldt 值:lru 字段的前 16bit,表示數(shù)據(jù)的訪問時(shí)間戳; counter 值:lru 字段的后 8bit,表示數(shù)據(jù)的訪問次數(shù)。
LFU這里用到了一個(gè)很有意思的設(shè)計(jì)。它采用的是8字段存儲(chǔ)訪問次數(shù)。我們得知8字節(jié)可以存放255次,如果超過255Redis如何響應(yīng)呢?
Redis寫滿之后,會(huì)使用LFU策略進(jìn)行數(shù)據(jù)淘汰。當(dāng)兩個(gè)值都是255時(shí),再去比較時(shí)間戳。但是以Redis的訪問請(qǐng)求量遠(yuǎn)遠(yuǎn)不夠。因此,在實(shí)現(xiàn) LFU 策略時(shí),Redis 并沒有采用數(shù)據(jù)每被訪問一次,就給對(duì)應(yīng)的 counter 值加 1 的計(jì)數(shù)規(guī)則,而是采用了一個(gè)更優(yōu)化的計(jì)數(shù)規(guī)則。
每當(dāng)數(shù)據(jù)被訪問一次時(shí),首先,用計(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。
當(dāng)?lfu_log_factor?為1時(shí),實(shí)際訪問次數(shù)為 100K 后,counter 值就達(dá)到 255 了,無法再區(qū)分實(shí)際訪問次數(shù)更多的數(shù)據(jù)了。
當(dāng) lfu_log_factor 取值為 10 時(shí),百、千、十萬級(jí)別的訪問次數(shù)對(duì)應(yīng)的 counter 值已經(jīng)有明顯的區(qū)分了(一般應(yīng)用時(shí),我們可以設(shè)置為10)
當(dāng)?lfu_log_factor?為100時(shí),當(dāng)實(shí)際訪問次數(shù)為 10M 時(shí),counter 值才達(dá)到 255,此時(shí),實(shí)際訪問次數(shù)小于 10M 的不同數(shù)據(jù)都可以通過 counter 值區(qū)分出來。
結(jié)尾
大概總結(jié)了
緩存和數(shù)據(jù)不一致性引發(fā)的問題 不同的緩存類型以及解決訪問。 Redis常見的生產(chǎn)問題,緩存雪崩,擊穿,穿透, 由穿透又聊到了布隆過濾器 緩存污染以及應(yīng)用措施,順帶的聊了一下LFU和LRU的經(jīng)典之處
學(xué)完之后,我只能驚嘆一下Redis作者太牛了,這種設(shè)計(jì)的思路也值得我們?nèi)蘸蟮南到y(tǒng)中借鑒一下。
這篇文章應(yīng)該沒有前幾篇好一些,這周我們公司的總監(jiān)臨時(shí)有事,然后我自己扛起了一面小旗,所有的學(xué)習(xí)時(shí)間都是每天晚上零散一些。周六白天又在給電商那套系統(tǒng)開發(fā)新的功能。晚上回憶了一下本周的知識(shí)碎片,敖到凌晨3點(diǎn)才完工的一篇。
每個(gè)知識(shí)點(diǎn)都是自己整理濃縮表達(dá)出來的,部分有些不容易懂的地方請(qǐng)及時(shí)指出,我們一起共同進(jìn)步!
因?yàn)榇_實(shí)比較累的緣故,部分知識(shí)出自蔣德均老師,Redis設(shè)計(jì)與實(shí)現(xiàn),Redis深度歷險(xiǎn)。尊重原創(chuàng)!
非常歡迎大家加我個(gè)人微信有關(guān)后端方面的問題我們在群內(nèi)一起討論!?我們下期再見!
長按上方掃碼二維碼,加我微信,拉你進(jìn)群

