redis總結(jié)

redis知識框架.png

1.redis基礎(chǔ)

今天我們來聊聊Redis緩存!
說起redis,首先想到的肯定是速度極快,因?yàn)閞edis是基于內(nèi)存的數(shù)據(jù)庫嘛!那么,redis除了速度快,還存在其他優(yōu)勢嗎?要知道,基于內(nèi)存的數(shù)據(jù)庫可不止redis一家啊,但它為什么可以廣受人們的青睞呢?來看看redis的優(yōu)勢:
1.redis存在多種數(shù)據(jù)結(jié)構(gòu),可供人們在不同的業(yè)務(wù)場景中使用
2.redis支持高可用、高并發(fā)和分布式
3.redis支持分布式鎖、消息隊(duì)列等技術(shù)
4.redis支持事務(wù)
所以說,在現(xiàn)在分布式泛濫的大環(huán)境下,如果不了解redis的相關(guān)技術(shù),那么肯定得被時代淘汰了!
要掌握redis,首先從redis的數(shù)據(jù)結(jié)構(gòu)開始說起吧:
key-value結(jié)構(gòu)(String):這是redis最常用的數(shù)據(jù)結(jié)構(gòu),也是我們最熟悉的結(jié)構(gòu)了吧,設(shè)置key和對應(yīng)的value 底層數(shù)據(jù)結(jié)構(gòu)采用的是SDS(簡單動態(tài)數(shù)組),解決字符串的安全性以及動態(tài)擴(kuò)容和壓縮問題,應(yīng)用場景一般為計數(shù)器等
list結(jié)構(gòu):底層數(shù)據(jù)結(jié)構(gòu)為壓縮列表和雙向鏈表,當(dāng)鍵值個數(shù)低于512以及保存的鍵值容量小于64kb時為壓縮列表,否則為雙向鏈表,這種數(shù)據(jù)結(jié)構(gòu)使得redis可以實(shí)現(xiàn)簡易的消息隊(duì)列
hash結(jié)構(gòu):類似java中的HashMap結(jié)構(gòu),也是K-V結(jié)構(gòu),這個的V又是一個鍵值對,底層數(shù)據(jù)結(jié)構(gòu)為散列表和壓縮鏈表,一般用于存儲對象
set結(jié)構(gòu):存儲一組數(shù)據(jù)不重復(fù)的元素,底層實(shí)現(xiàn)為有序數(shù)組和散列表 ,應(yīng)用場景一般用于求并集等
zset結(jié)構(gòu):set集合的有序版,使用一個權(quán)重分?jǐn)?shù)實(shí)現(xiàn)有序。底層結(jié)構(gòu)是散列表和跳表,應(yīng)用場景如直播間的在線用戶實(shí)時排行,禮物榜等有序情況
再額外補(bǔ)充一種數(shù)據(jù)結(jié)構(gòu)——bitmap(位圖):通常用來0,1來存儲,可以用來表示狀態(tài)。實(shí)現(xiàn)了壓縮空間的作用,要知道1Mb = 1024KB,1KB = 1024B 1B = 8bit,一般可以用來判斷用戶登錄狀態(tài),是否打卡等狀態(tài)的表示,非常好用!

2.redis的線程模型

我們都知道redis是一個單線程模型,但是網(wǎng)上又有很多人說redis6.0之后出現(xiàn)了多線程,那么redis到底是單線程還是多線程模型呢?這里可以劃分為兩部分來講解:多線程I/O讀取部分、單線程執(zhí)行部分。
多線程I/O讀?。?/em>redis是基于Reactor模式的,采用IO多路復(fù)用技術(shù)用來監(jiān)聽來自客戶端的大量線程讀取請求,并將其按事件先后順序放入任務(wù)隊(duì)列,等待文件處理器處理。在6.0之后,redis新增了多線程刪除大容量key、清除過期緩存等操作,大大提升了IO性能和讀寫效率
單線程執(zhí)行命令:在處理過程,redis依然是使用單線程處理任務(wù)隊(duì)列中的讀取請求,所以,是線程安全的;有同學(xué)可能會問了——為什么redis不在執(zhí)行命令時也是用多線程呢?這樣不是可以更大程度上提高效率嗎?
這個問題其實(shí)官方團(tuán)隊(duì)也是想過的,但是考慮到redis性能的瓶頸是在內(nèi)存和網(wǎng)絡(luò)IO上面,與CPU其實(shí)關(guān)系不大,使用多線程還會因?yàn)榫€程上下文切換造成額外的開銷,甚至有可能造成負(fù)優(yōu)化;而且,redis本身使用pipline處理命令就很快,實(shí)在沒必要上多線程。

3.redis內(nèi)存管理

補(bǔ)充:前提知識點(diǎn)——redis有一個過期字典,這個字典的鍵指向的是redis數(shù)據(jù)庫中的key值,value是一個時間戳,即我們設(shè)置的過期時間,是一個Long型,通過這個過期字典,可以判斷key是否過期;

假設(shè)一下,我們系統(tǒng)分配給redis的內(nèi)存有2G,在一個頻繁查詢數(shù)據(jù)的系統(tǒng)中,大量數(shù)據(jù)可能沒到幾小時就把redis的內(nèi)存給撐滿了,這個時候,redis是不是為了自身的可靠性和安全性,試圖去刪除一些已經(jīng)存在的緩存呢?那么,他又會去刪除那些緩存呢?刪除的策略又會有哪些呢?這就需要我們來具體了解一下——redis的內(nèi)存管理了!

解答①當(dāng)我們加載緩存時,一般會為存儲進(jìn)來的數(shù)據(jù)設(shè)置一個過期時間,在正常情況下,只要過期時間一到,這個數(shù)據(jù)就會被刪除。刪除過期數(shù)據(jù)會有兩種方式;第一:惰性刪除,在key取出來的時候?qū)ζ渥鲞^期檢查——CPU友好(這樣可能存在大量key存在于內(nèi)存中沒被刪除),第二:定期刪除,redis定期抽取一部分key對其做過期檢查,并且也會限制刪除時間以減少CPU使用,提高用戶體驗(yàn);那么非正常情況呢?就比如我們說的,某一時刻突然加載一個大容量數(shù)據(jù)或者是redis本身就保留了很多過期key沒有刪除,造成redis內(nèi)存不夠用了,這時redis怎么處理呢?
解答②當(dāng)redis內(nèi)存不夠用時,這時它會嘗試刪除一些數(shù)據(jù),這里的刪除其實(shí)可以換一個說法——淘汰!說到淘汰,那就得介紹幾種淘汰策略了:

volatile-lru(least recently used):從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最近最少使用的數(shù)據(jù)淘汰
volatile-ttl:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選將要過期的數(shù)據(jù)淘汰
volatile-random:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中任意選擇數(shù)據(jù)淘汰
allkeys-lru(least recently used):當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,移除最近最少使用的 key(這個是最常用的)
allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰
no-eviction:禁止驅(qū)逐數(shù)據(jù),也就是說當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯。這個應(yīng)該沒人使用吧!
4.0 版本后增加以下兩種:
volatile-lfu(least frequently used):從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最不經(jīng)常使用的數(shù)據(jù)淘汰
allkeys-lfu(least frequently used):當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中,移除最不經(jīng)常使用的 key

4.redis持久化

說完內(nèi)存,接下來就得考慮一下redis是如何持久化保存數(shù)據(jù)了!
redis有兩種方式持久化保存數(shù)據(jù)——RDB、AOF
RDB方式:這種方式簡單來理解就是——做快照;根據(jù)設(shè)置好的定時或者手動去執(zhí)行BSAVE命令,生成一個RDB文件,重啟時使用會運(yùn)行這個快照文件將數(shù)據(jù)恢復(fù)。具體來說,redis每隔一段時間會執(zhí)行一次BSAVE或者SAVE命令去生成一個快照文件,執(zhí)行BSAVE命令時,這是用戶主線程會中斷,去fork一個子進(jìn)程,fork完成之后,主線程繼續(xù)工作,由子進(jìn)程去執(zhí)行生成RDB文件的任務(wù)。(很容易想到,這種方式是不是存在數(shù)據(jù)缺失的問題?對的,當(dāng)服務(wù)器在兩次生成快照的時間間隔內(nèi)宕機(jī)了,這是第一次快照之后的那些數(shù)據(jù)就會全部丟失?。。。?br> AOF方式:從第一種方式中我們可以看出,RDB方式存在時間間隔,造成了延時性。這是就有了AOF方式——對操作指令進(jìn)行日志記錄,在規(guī)定時間內(nèi)進(jìn)行刷盤操作,通過redis配置刷盤頻率:從不/一秒一次/有數(shù)據(jù)修改就寫入;重啟時只需要讀取這個日志文件即可恢復(fù)數(shù)據(jù)。AOF具備了數(shù)據(jù)實(shí)時性!
拓展:我們一般執(zhí)行的AOF日志文件一般是經(jīng)過redis“加工”處理過的——AOF重寫;詳細(xì)過程如下:
①后臺啟動一個線程將命令異步寫入AOF文件
②redis會在fork一個子進(jìn)程去復(fù)制AOF文件,形成新的AOF文件
③在子進(jìn)程復(fù)制的過程中,因?yàn)橹骶€程還在執(zhí)行命令,所以這段時間時間內(nèi)主線程執(zhí)行的命令會放入緩存區(qū)中
④在fork完子進(jìn)程后再將緩存區(qū)中的命令寫入新的AOF文件,完成重寫
在redis4.0之后,支持混合方式——AOF與RDB方式并存,具體使用那種持久化方式得看具體的業(yè)務(wù)場景

5.redis集群

做過項(xiàng)目的應(yīng)該有所體會,一般我們需要使用redis的業(yè)務(wù)環(huán)境,基本不會是單機(jī)環(huán)境。業(yè)務(wù)決定技術(shù),于是也就有了redis集群,嚴(yán)格來說,redis集群并不屬于開發(fā)層面的問題,更多的是與運(yùn)維有關(guān)。那么在redis集群環(huán)境下,如何保證我們的應(yīng)用高可用呢?這就是接下來我們要討論的問題了!下面我們介紹幾種常見的集群架構(gòu)

1.主從架構(gòu):應(yīng)該是大家聽得最多的架構(gòu)了,master-slave模式;一個主服務(wù)器用于服務(wù),其余從服務(wù)器做數(shù)據(jù)備份,毫無疑問,既然選擇這種架構(gòu),必然牽扯到數(shù)據(jù)一致性的問題~那么redis是如何實(shí)現(xiàn)主從架構(gòu)下的數(shù)據(jù)一致性的呢?

補(bǔ)充:redis數(shù)據(jù)同步有兩種方式——完全重同步和部分重同步

詳細(xì)步驟:主從服務(wù)器實(shí)現(xiàn)通信的過程中,會攜帶兩個參數(shù)——RUNID(機(jī)器運(yùn)行的id,用于身份校驗(yàn))、offset(同步進(jìn)度參數(shù)),首先,通信是根據(jù)RUNID進(jìn)行必要的身份和權(quán)限認(rèn)證,驗(yàn)證完成之后,如果是第一次復(fù)制,那么offer是沒有的,表示當(dāng)前采用完全重同步方式進(jìn)行數(shù)據(jù)同步,當(dāng)?shù)诙卧龠M(jìn)行復(fù)制時,這時通信雙方服務(wù)器會有offset參數(shù),在進(jìn)行復(fù)制時,會對offset參數(shù)進(jìn)行校驗(yàn)以此來判斷進(jìn)度,根據(jù)二者之間的差值來選擇性的同步數(shù)據(jù),也叫部分重同步。


注意:這里會存在找不到offset參數(shù)被覆蓋找不到的情況,原因是主服務(wù)器記錄offset參數(shù)的方式是一個環(huán)形buffer,有可能會被覆蓋。如果offset參數(shù)被覆蓋,這時還是會采用完全重同步的方式。
數(shù)據(jù)一致性說完,再來考慮一個問題,當(dāng)主服務(wù)器出現(xiàn)故障后,從服務(wù)器是如何切換成主服務(wù)器的呢?——高可用
答案是哨兵模式?。。?/p>

哨兵模式:先用通俗一點(diǎn)的話來講,哨兵就是監(jiān)督redis服務(wù)器情況的服務(wù)器,為了高可用,所以采用的也是集群模式。每一個哨兵會不間斷的去ping主服務(wù)器,以此來判斷主服務(wù)器是否宕機(jī),發(fā)生故障。一但發(fā)現(xiàn)主服務(wù)器發(fā)生故障,這時就會哨兵們會先選出一個領(lǐng)頭哨兵,由領(lǐng)頭哨兵重新挑選一個新主服務(wù)器,然后通知其他服務(wù)器與新的主服務(wù)器建立主從關(guān)系,開啟數(shù)據(jù)同步。(在這個過程中不可避免會造成部分的數(shù)據(jù)丟失)
簡單說一下可能造成數(shù)據(jù)丟失的原因:
一,主服務(wù)器“假死”,由于網(wǎng)絡(luò)原因?qū)е律诒恢睙o法檢測到主服務(wù)器,當(dāng)固定時間類無法與主服務(wù)器通信時,判斷主服務(wù)器發(fā)生故障,但實(shí)際上主服務(wù)器還是在接收用戶發(fā)送的數(shù)據(jù),此時,當(dāng)新的主服務(wù)器出現(xiàn)后,原來的主服務(wù)器就變成了從服務(wù)器,這期間用戶發(fā)送給原主服務(wù)器的數(shù)據(jù)也就丟失了
二、主從服務(wù)器在進(jìn)行數(shù)據(jù)同步時,突然宕機(jī),這時數(shù)據(jù)還未同步完,新的主服務(wù)器數(shù)據(jù)不完整,也會造成數(shù)據(jù)丟失

2.分片集群

首先還是先介紹一下分片集群的背景,上次提高主從架構(gòu)模型,一個主服務(wù)器負(fù)責(zé)讀寫操作,剩下的從服務(wù)器用于讀操作和復(fù)制數(shù)據(jù),當(dāng)主服務(wù)器內(nèi)存無法滿足業(yè)務(wù)需求時,單純依靠redis的內(nèi)存管理是無法滿足業(yè)務(wù)的,這時,就必須考慮縱向擴(kuò)容了,當(dāng)多個redis服務(wù)器存儲數(shù)據(jù),多個服務(wù)器合起來的數(shù)據(jù)即為系統(tǒng)全量數(shù)據(jù)——分片集群的由來。
分片集群一般存在兩種模式:redis cluster模式以及客戶端模式(codis)
1.redis cluster模式
過程:這個模式是基于客戶端的,首先服務(wù)器會初始化16384個哈希槽位,然后根據(jù)算法對服務(wù)器個數(shù)取余,分配給每個服務(wù)器一部分哈希槽位,在后續(xù)數(shù)據(jù)復(fù)制或者數(shù)據(jù)遷移時都是槽位個數(shù)為單位進(jìn)行的,所有服務(wù)器分配完這些槽位之后,隨即問題就出現(xiàn)了——客戶端進(jìn)行讀寫操作的時候怎么知道去操作那一臺服務(wù)器呢?換句話說,怎樣才能順利找到我們想要的數(shù)據(jù)呢?很簡單,要想查找想要的數(shù)據(jù),無非就是要知道數(shù)據(jù)存放在那個哈希槽上,那么必然得建立一個路由映射關(guān)系,使得我們可以順利通過服務(wù)器——> 哈希槽。所以,在分配完哈希槽之后,所以實(shí)例之間會進(jìn)行通信,告訴其他實(shí)例自己所負(fù)責(zé)的哈希槽,就這樣,所有實(shí)例都會建立一張路由管理表,當(dāng)客戶端第一次發(fā)送請求時,服務(wù)器會通過路與表去找到對應(yīng)的實(shí)例返回數(shù)據(jù),后面客戶端也會緩存一張路由關(guān)系表,后續(xù)請求就直接使用本地緩存的路由表進(jìn)行查找了


數(shù)據(jù)遷移:為什么會產(chǎn)生數(shù)據(jù)遷移?不外乎就是redis實(shí)例故障(減少),或者新增redis實(shí)例(增加);不管是增加還是減少,都需要發(fā)生數(shù)據(jù)遷移,下面我們就來了解一下數(shù)據(jù)遷移的詳細(xì)過程:
之前我們說過,在數(shù)據(jù)復(fù)制或者數(shù)據(jù)遷移時我們都是以槽位為單位來實(shí)現(xiàn)的,所以,大家可以想象一下,當(dāng)發(fā)生數(shù)據(jù)遷移時,首先是不是得知道需要遷移那些哈希槽的數(shù)據(jù)呢?當(dāng)確定這些哈希槽位之后,下一步就是重新將這些哈希槽分配給已有的實(shí)例了,然后上面所說的那張路由關(guān)系表就會發(fā)生變化,所有實(shí)例之間進(jìn)行通訊更新這張表,當(dāng)客戶端發(fā)送數(shù)據(jù)請求時,如果數(shù)據(jù)已經(jīng)遷移完成,會返回move命令告訴客戶端應(yīng)該想那臺新的redis服務(wù)器發(fā)送請求了,同時更新本地的緩存表。如果數(shù)據(jù)還在遷移過程中,則會返回ask命令讓客戶端去找對應(yīng)的redis實(shí)例。

2.服務(wù)端路由模式
相對于把路由信息放入每個實(shí)例以及客戶端,服務(wù)端路由——顧名思義,采用的思想是在一個服務(wù)端維護(hù)路由關(guān)系,客戶端發(fā)送請求時,先通過這個服務(wù)端,由這個代理確定應(yīng)該講請求發(fā)送到到那臺redis實(shí)例。說白了,就是有一個中間商幫你維護(hù)路由關(guān)系表,要用的時候找這個中間商就行了!
一般用的是codis,具體實(shí)現(xiàn)如下,在啟動時,會先將所有實(shí)例注冊到zookeeper集群,這里需要注意的是,codis只會初始化1024個哈希槽分配給redis實(shí)例;客戶端發(fā)送請求,codis proxy代理找到相關(guān)實(shí)例,將數(shù)據(jù)返回;


當(dāng)需要擴(kuò)容時(引入新的redis實(shí)例),codis支持兩種數(shù)據(jù)遷移方式——同步和異步遷移;
同步遷移:①原實(shí)例將對應(yīng)哈希槽上的數(shù)據(jù)發(fā)送給目標(biāo)實(shí)例,②目標(biāo)實(shí)例接收完數(shù)據(jù)之后向原實(shí)例發(fā)送ack命令,③原實(shí)例收到ack命令之后刪除哈希槽數(shù)據(jù)。遷移完成!
異步遷移:在上面步驟二時,不用等目標(biāo)實(shí)例發(fā)送ack命令,原實(shí)例會接收客戶端請求,但是此時未遷移的數(shù)據(jù)會標(biāo)記為只讀,當(dāng)客戶端發(fā)送寫請求時是不支持的,會讓客戶端選擇重試,最后會寫到目標(biāo)實(shí)例上!
big key問題:數(shù)據(jù)中,不可避免會出現(xiàn)大容量數(shù)據(jù)問題,這時如果一次性的遷移這條數(shù)據(jù)耗時一定非常長,那么怎么辦呢?在異步遷移時,采用了指令分割的策略,舉個栗子,假設(shè)這個key有十萬條命令,在發(fā)送時采用一條一條的發(fā)送指令,而不是將十萬條數(shù)據(jù)先壓縮打包再一次性發(fā)送


額外擴(kuò)展——事務(wù)和分布式鎖 (非主干知識點(diǎn),后面說到分布式的時候回詳細(xì)講)
事務(wù):之前將Mysql的時候,大家對這個詞應(yīng)該不陌生了,當(dāng)一個事務(wù)發(fā)生時,事務(wù)內(nèi)的操作要不全執(zhí)行,要么全不執(zhí)行;在redis中也是同樣的道理,這里需要引入三個命令:multi,exec,discard,分別代表事務(wù)開啟、事務(wù)執(zhí)行和事務(wù)丟棄。在進(jìn)行事務(wù)操作時,首先將命令寫入隊(duì)列,等到執(zhí)行exec命令時把隊(duì)列中的命令進(jìn)行寫操作,如果discard則清空隊(duì)列。存在兩種操作失敗的情況——①命令入隊(duì)列時錯誤,此時執(zhí)行exec命令則全部命令執(zhí)行無效;②命令exec時出現(xiàn)錯誤,此時只有錯誤命令執(zhí)行失敗,其他命令正常執(zhí)行

分布式鎖:這個詞可以拆分成兩部分,分布式+鎖;什么是分布式?通俗點(diǎn)說,酒店做一個菜需要很多人分工協(xié)作,有人負(fù)責(zé)提供食材,有人負(fù)責(zé)處理食材,有人負(fù)責(zé)烹飪食材,有人負(fù)責(zé)擺盤,最后這道菜才出現(xiàn)在你面前;分布式就是這么個意思了,做一個訂單服務(wù),要用到商品服務(wù)、下單服務(wù)、短信服務(wù)等,而這些服務(wù)在不同的服務(wù)器上運(yùn)行,為了保證一個事務(wù)的正常進(jìn)行,我們需要對其加鎖處理!鎖——對共享資源進(jìn)行互斥限制,保證同一時刻只有一個服務(wù)使用該資源,保證事務(wù)正確!實(shí)現(xiàn)方式一般有三種:數(shù)據(jù)庫鎖、redis分布式鎖、zookeeper分布式鎖(感興趣的同學(xué)可以先去了解一下,后面我會專門寫一篇關(guān)于分布式的文章,到時候詳細(xì)討論一下這幾種鎖!)

6.生產(chǎn)問題

在生產(chǎn)中,redis一般存在三個問題——緩存擊穿、緩存穿透、緩存雪崩!
先有一個直觀了解,這三個問題其本質(zhì)都是緩存命中率問題——用戶發(fā)起請求時,在緩存中未找到數(shù)據(jù),從而直接訪問數(shù)據(jù),導(dǎo)致數(shù)據(jù)庫負(fù)載過大!下面在來詳細(xì)討論一下這幾個問題吧
緩存擊穿:某些熱點(diǎn)key承載了大量高并發(fā)請求,在某一時刻失效后,導(dǎo)致大量請求直接訪問數(shù)據(jù)庫
緩存穿透:用戶發(fā)起的大量請求key都在緩存和數(shù)據(jù)庫中都不存在
緩存雪崩:在同一時刻,緩存中大量key失效,緩存無法命中!

解決方案
對于擊穿:我們可以對熱點(diǎn)key進(jìn)行延期——在key快過期的時候,我們可以開啟一個線程延長這個key的過期時間
對于穿透:我們可以在緩存中這這些不存在的key設(shè)置一個null值,從而避免訪問數(shù)據(jù)庫,或者使用布隆過濾器判斷key是否存在,先過濾一遍;
對于雪崩:和擊穿的思路相似,將key的失效時間離散化,這樣避免同一時間大量key全部失效,導(dǎo)致數(shù)據(jù)庫壓力過大

最后的最后——聊聊生產(chǎn)過程中如何保證緩存與數(shù)據(jù)庫的數(shù)據(jù)一致性!

數(shù)據(jù)一致性,這個問題展開來講可以說很多內(nèi)容,比如mysql主從架構(gòu)數(shù)據(jù)一致性,redis主從架構(gòu)數(shù)據(jù)一致性,各種中間件數(shù)據(jù)一致性,而在這里,我所說的僅僅是redis緩存與數(shù)據(jù)庫之間的數(shù)據(jù)一致性——兩個核心問題:操作一致性和高并發(fā)
操作一致性:我們知道,在數(shù)據(jù)更新時通常使用redis無非就會兩種操作,1.操作redis數(shù)據(jù),更新數(shù)據(jù)庫2.更新數(shù)據(jù)庫,再操作redis,不論順序如何,二者之間必然是要么都成功,要么都失敗,操作上必須一致,否則必然導(dǎo)致數(shù)據(jù)不一致。(注意上面是操作兩個字,后面我們會講具體操作)
高并發(fā):(這里我們以先更新數(shù)據(jù)庫,再更新緩存為例)當(dāng)存在多個事務(wù)時,事務(wù)A更新數(shù)據(jù)庫,此時尚未操作緩存,這時事務(wù)B進(jìn)來,讀取緩存數(shù)據(jù)并返回,緊接著事務(wù)A操作緩存,不管A是如何操作緩存的,此時事務(wù)B讀取的數(shù)據(jù)必然是臟數(shù)據(jù),可見,高并發(fā)場景下,我們必然需要一種策略維護(hù)數(shù)據(jù)一致性。


那么,帶著這兩個問題,我們考慮該如何解決~
高并發(fā)問題:這里就可以講講上面說到的——操作redis;
我們考慮一下,在開發(fā)過程中,我們?yōu)榱吮WC數(shù)據(jù)一致性,是不是會與兩種考慮,第一種——更新redis;第二種——刪除redis;
這兩種方案,想想是都可以保證數(shù)據(jù)一致的,第一種在寫操作的時候相對來說麻煩點(diǎn),所以在讀的時候就不需要操作redis;第二種采用刪除redis,那么在讀數(shù)據(jù)的時候,必然就需要我們把新數(shù)據(jù)寫入緩存;后邊這種方案其實(shí)就是我們說的——旁路緩存模式;究竟哪一種更好呢?
我們不妨從兩個角度來思考一下:
從緩存命中率來說——通常,需要更新的數(shù)據(jù)大概率不屬于熱數(shù)據(jù),更新完成之后可能查詢并不頻繁,造成緩存利用率不高!
從性能上面來說——更新緩存可能涉及到分布式鎖,一旦涉及到鎖,那么效率和性能肯定下降,而且更新操作相對刪除來說更產(chǎn)生并發(fā)問題。要解決必然需要引入相關(guān)技術(shù)或者中間件,增加維護(hù)成本。
綜上,刪除redis——可能更好一些!


那么既然選擇刪除redis,又得進(jìn)一步考慮兩種操作了,實(shí)現(xiàn)更新數(shù)據(jù)庫再刪除redis,還是先刪除redis再更新數(shù)據(jù)庫呢?
從并發(fā)角度來解析:
1.先刪除緩存,再更新數(shù)據(jù)庫
線程A要更新數(shù)據(jù)(某字段信息Info = 1)—->線程A讀取數(shù)據(jù)->線程A刪除緩存->線程B在緩存中未讀到數(shù)據(jù),在DB中讀取->線程B更新緩存數(shù)據(jù)(Info = 1)->線程A更新緩存數(shù)據(jù)(Info = 2);
此時緩存中Info = 1,而實(shí)際數(shù)據(jù)庫中數(shù)據(jù)為2,數(shù)據(jù)不一致

2.先更新數(shù)據(jù)庫,再刪除緩存
緩存中不存在數(shù)據(jù)—->線程A讀取數(shù)據(jù)(某字段信息Info = 1)->線程B在緩存中未讀到數(shù)據(jù),在DB中讀取->線程B更新數(shù)據(jù)(Info = 2)->線程B刪除緩存->線程A更新緩存數(shù)據(jù)(Info = 1);
同樣,此時發(fā)生數(shù)據(jù)不一致!
但是,請大家仔細(xì)想想2中的情況,其實(shí)它發(fā)生的概率極低,必須滿足三個條件:
1.緩存中不存在數(shù)據(jù) 2.同一條數(shù)據(jù)讀和寫并發(fā) 3.更新數(shù)據(jù)庫+刪除緩存的時間比讀數(shù)據(jù)庫+更新緩存的時間要短
尤其是最后一條,我們都知道,在更新數(shù)據(jù)庫操作時會加排它鎖,此時更新數(shù)據(jù)的時間必然要比讀數(shù)據(jù)的時間長;
所以,最終選擇采用——更新數(shù)據(jù)庫+刪除緩存;

說清楚高并發(fā)問題解決方案后,再來聊聊第一個問題——操作一致性;
要保證更新數(shù)據(jù)庫+刪除緩存的操作原子性,最核心的問題就是——失敗重試;
不管是那一步操作,只要操作失敗就進(jìn)行重試,直到成功;這里大家應(yīng)該都能Get到吧,再往下走我們就得講講重試的方式了!

1.消息隊(duì)列
一般我們將操作數(shù)據(jù)庫這一步放入消息隊(duì)列中,然后操作redis刪除緩存,這種重試策略比較常見,而且能夠保證重試的可靠性,避免消息丟失!
2.訂閱binlog日志推送
采用中間件訂閱binlog日志變更記錄,發(fā)生變更后由中間件推送到MQ中,以canal為例:


訂閱日志變更.png

在這種方式中,我們并不需要關(guān)注redis,只需要更新數(shù)據(jù)庫即可!由中間件執(zhí)行后續(xù)步驟。

常見的三種數(shù)據(jù)一致性策略:
旁路緩存策略——讀:讀取緩存,若緩存中沒有則查詢數(shù)據(jù)庫并更新緩存,返回數(shù)據(jù);寫——更新數(shù)據(jù)庫,刪除緩存;
讀寫穿透策略——讀:在cache中讀取,如果存在直接返回,如果不存在由cache去查找數(shù)據(jù)庫并寫入緩存;寫:寫入cache,由cache寫入數(shù)據(jù)庫;
異步讀寫策略——和讀寫穿透策略相似,只不過讀寫穿透是同步操作,而這個策略是異步操作。

好了,以上就是redis的全部內(nèi)容了!

面試總結(jié)系列第五面——?dú)g迎留言討論,共同進(jìn)步!*

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • [TOC] 一、Redis 基礎(chǔ)常問 1.1、Redis 有哪些數(shù)據(jù)結(jié)構(gòu) 基礎(chǔ):字符串String、字典Hash、...
    w1992wishes閱讀 793評論 0 1
  • Redis為什么速度快 1、完全基于內(nèi)存,絕大部分請求是純粹的內(nèi)存操作,非常快速。數(shù)據(jù)存在內(nèi)存中,類似于HashM...
    亖狼何需裝羴閱讀 920評論 0 0
  • Redis使用場景 String 計數(shù)器 (排行榜,閱讀量,瀏覽量等等)(incr decr) Web集群的ses...
    GGBond_8488閱讀 275評論 0 1
  • Redis數(shù)據(jù)結(jié)構(gòu) String字符串 Hash存儲對象 List微博關(guān)注列表、消息列表雙向鏈表,支持反向查找和遍...
    幽游不想吃飯閱讀 416評論 0 0
  • 目錄 五種數(shù)據(jù)結(jié)構(gòu) 簡單動態(tài)字符串sds 這種數(shù)據(jù)結(jié)構(gòu)的應(yīng)用舉例,設(shè)置過期時間,發(fā)短信的時候,設(shè)置驗(yàn)證碼的過期時間...
    后來丶_a24d閱讀 295評論 0 7

友情鏈接更多精彩內(nèi)容