高并發(fā)存儲(chǔ)番外篇:Redis套路,一網(wǎng)打盡


本文內(nèi)容提要
Redis為什么這么快
1.1. 數(shù)據(jù)結(jié)構(gòu)SDS的妙用
1.2. 性能優(yōu)良的事件模型驅(qū)動(dòng)
1.3. 基于內(nèi)存的操作Redis為什么這么靠譜
2.1. AOF持久化
2.2. RDB持久化
2.3. Sentinel高可用Redis6.x多線程一覽 Redis最佳實(shí)踐
Part1Redis為什么這么快
1.1數(shù)據(jù)結(jié)構(gòu)SDS的妙用
我們知道redis的底層是用c語(yǔ)言來(lái)編寫(xiě)的,但是,數(shù)據(jù)結(jié)構(gòu)確沒(méi)有直接套用C的結(jié)構(gòu),而是根據(jù)redis的定位自建了一套數(shù)據(jù)結(jié)構(gòu)。
C語(yǔ)言中的字符串結(jié)構(gòu):
SDS定義下的字符串結(jié)構(gòu):
可以看到,相比于C語(yǔ)言來(lái)說(shuō),也就多了幾個(gè)字段,分別用來(lái)標(biāo)識(shí)空閑空間和當(dāng)前數(shù)據(jù)長(zhǎng)度,但簡(jiǎn)直是神來(lái)之筆:
可以O(shè)(1)復(fù)雜度獲取字符串長(zhǎng)度;有len字段的存在,無(wú)需像C結(jié)構(gòu)一樣遍歷計(jì)數(shù)。 杜絕緩存區(qū)溢出;C字符串不記錄已占用的長(zhǎng)度,所以需要提前分配足夠空間,一旦空間不夠則會(huì)溢出。而有free字段的存在,讓SDS在執(zhí)行前可以判斷并分配足夠空間給程序 減少字符串修改帶來(lái)的內(nèi)存重分配次數(shù);有free字段的存在,使SDS有了空間預(yù)分配和惰性釋放的能力。 對(duì)二進(jìn)制是安全的;二進(jìn)制可能會(huì)有字符和C字符串結(jié)尾符 '\0' 沖突,在遍歷和獲取數(shù)據(jù)時(shí)產(chǎn)生截?cái)喈惓#鳶DS有了len字段,準(zhǔn)確了標(biāo)識(shí)了數(shù)據(jù)長(zhǎng)度,不需擔(dān)心被中間的 '\0' 截?cái)唷?/span>
上面的內(nèi)容以字符串來(lái)說(shuō)明SDS和C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)的差異和優(yōu)勢(shì)。順便來(lái)看看鏈表、hash表、跳表分別被Redis設(shè)計(jì)成了什么樣的數(shù)據(jù)結(jié)構(gòu):
<<< 左右滑動(dòng)見(jiàn)更多 >>>
可以看到,Redis在設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)的時(shí)候出發(fā)點(diǎn)是一致的。總結(jié)起來(lái)就是一句話:空間換時(shí)間。
用犧牲存儲(chǔ)空間和微小的計(jì)算代價(jià),來(lái)?yè)Q取數(shù)據(jù)的快速操作
1.2性能優(yōu)良的事件驅(qū)動(dòng)模式
redis6.x之前,一直在說(shuō)單線程如何如之何的好。
那么,具體單線程體現(xiàn)在哪里,又是怎么完成數(shù)據(jù)讀寫(xiě)工作的呢?
$ 單線程
關(guān)于新版本的多線程模型在后面小節(jié)單獨(dú)說(shuō),這里先說(shuō)單線程。
所謂單線程是指對(duì)數(shù)據(jù)的所有操作都是由一個(gè)線程按順序挨個(gè)執(zhí)行的,使用單線程可以:
避免了不必要的上下文切換和競(jìng)爭(zhēng)條件,也不存在多進(jìn)程或者多線程導(dǎo)致的切換而消耗CPU; 不用去考慮各種鎖的問(wèn)題,不存在加鎖釋放鎖操作,沒(méi)有因?yàn)榭赡艹霈F(xiàn)死鎖而導(dǎo)致的性能消耗。
然而,使用了單線程的處理方式,就意味著到達(dá)服務(wù)端的請(qǐng)求不可能被立即處理。
那么怎么來(lái)保證單線程的資源利用率和處理效率呢?
$ IO多路復(fù)用和事件驅(qū)動(dòng)
Redis服務(wù)端,從整體上來(lái)看,其實(shí)是一個(gè)事件驅(qū)動(dòng)的程序,所有的操作都以事件的方式來(lái)進(jìn)行。

如圖所示,Redis的事件驅(qū)動(dòng)架構(gòu)由套接字、I/O多路復(fù)用、文件事件分派器、事件處理器四個(gè)部分組成:
套接字(Socket),是對(duì)網(wǎng)絡(luò)中不同主機(jī)上的應(yīng)用進(jìn)程之間進(jìn)行雙向通信的端點(diǎn)的抽象。
I/O多路復(fù)用,通過(guò)監(jiān)視多個(gè)描述符,當(dāng)描述符就緒,則通知程序進(jìn)行相應(yīng)的操作,來(lái)幫助單個(gè)線程高效的處理多個(gè)連接請(qǐng)求。
Redis為每個(gè)IO多路復(fù)用函數(shù)都實(shí)現(xiàn)了相同的API,因此,底層實(shí)現(xiàn)是可以互換的。
Reids默認(rèn)的IO多路復(fù)用機(jī)制是epoll,和select/poll等其他多路復(fù)用機(jī)制相比,epoll具有諸多優(yōu)點(diǎn):
| 并發(fā)連接限制 | 內(nèi)存拷貝 | 活躍連接感知 | |
|---|---|---|---|
| epoll | 沒(méi)有最大并發(fā)連接的限制 | 共享內(nèi)存,無(wú)需內(nèi)存拷貝 | 基于event callback方式,只感知活躍連接 |
| select | 受fd限制,32位機(jī)默認(rèn)1024個(gè)/64位機(jī)默認(rèn)2048個(gè) | 把fd集合從用戶(hù)態(tài)拷貝到內(nèi)核態(tài) | 只能感知有fd就緒,但無(wú)法定位,需要遍歷+輪詢(xún) |
| poll | 采用鏈表存儲(chǔ)fd無(wú)最大并發(fā)連接數(shù)限制 | 同select | 同select,需遍歷+輪詢(xún) |
事件驅(qū)動(dòng),Redis設(shè)計(jì)的事件分為兩種,文件事件和時(shí)間事件,文件事件是對(duì)套接字操作的抽象,而時(shí)間事件則是對(duì)一些定時(shí)操作的抽象。
文件事件:
客戶(hù)端連接請(qǐng)求(AE_READABLE事件) 客戶(hù)端命令請(qǐng)求(AE_READABLE事件)和事 服務(wù)端命令回復(fù)(AE_WRITABLE事件)
時(shí)間事件: 分為定時(shí)事件和周期性時(shí)間;redis的所有時(shí)間事件都存放在一個(gè)無(wú)序鏈表中,當(dāng)時(shí)間事件執(zhí)行器運(yùn)行時(shí),需要遍歷鏈表以確保已經(jīng)到達(dá)時(shí)間的事件被全部處理。
可以看到,Redis整個(gè)執(zhí)行方案是通過(guò)高效的I/O多路復(fù)用件驅(qū)動(dòng)方式加上單線程內(nèi)存操作來(lái)達(dá)到優(yōu)秀的處理效率和極高的吞吐量。
1.3基于內(nèi)存的操作
上面的小節(jié)也提到了,redis之所以可以使用單線程來(lái)處理,其中的一個(gè)原因是,內(nèi)存操作對(duì)資源損耗較小,保證了處理的高效性。
如此寶貴的內(nèi)存資源,Redis是怎么維護(hù)和管理的呢?
$ 除了增刪改查還有哪些維護(hù)性操作[1]
命中率統(tǒng)計(jì),在讀取一個(gè)鍵之后,服務(wù)器會(huì)根據(jù)鍵是否存在來(lái)更新服務(wù)器的鍵空間命中次數(shù)或鍵空間不命中次數(shù)。
LRU時(shí)間更新,在讀取一個(gè)鍵之后,服務(wù)器會(huì)更新鍵的LRU時(shí)間,這個(gè)值可以用于計(jì)算鍵的閑置時(shí)間。
惰性刪除,如果服務(wù)器在讀取一個(gè)鍵時(shí)發(fā)現(xiàn)該鍵已經(jīng)過(guò)期,那么服務(wù)器會(huì)先刪除這個(gè)過(guò)期鍵,然后才執(zhí)行余下的其他操作。
鍵的dirty標(biāo)識(shí),如果有客戶(hù)端使用WATCH命令監(jiān)視了該鍵,服務(wù)器會(huì)將這個(gè)鍵標(biāo)記為dirty,讓事務(wù)程序注意到這個(gè)鍵已經(jīng)被修改過(guò)。每次修改都會(huì)對(duì)dirty加一,用于觸發(fā)持久化和復(fù)制。
數(shù)據(jù)庫(kù)通知,“如果服務(wù)器開(kāi)啟了數(shù)據(jù)庫(kù)通知功能,那么在對(duì)鍵進(jìn)行修改之后,服務(wù)器將按配置發(fā)送相應(yīng)的數(shù)據(jù)庫(kù)通知”
$ Redis何如管理內(nèi)存
過(guò)期鍵刪除,內(nèi)存和CPU資源都是寶貴的,Redis通過(guò)定期刪除設(shè)定合理的執(zhí)行時(shí)長(zhǎng)和執(zhí)行頻率,配合惰性刪除兜底的方式,來(lái)達(dá)到CPU時(shí)間占用和內(nèi)存浪費(fèi)之間的平衡。
數(shù)據(jù)淘汰,如果key生產(chǎn)的太快,定期刪除操作跟不上新生產(chǎn)的速率,而這些key又很少被訪問(wèn)無(wú)法觸發(fā)惰性刪除,是否會(huì)把內(nèi)存撐爆?回答是不會(huì),因?yàn)閞edis有數(shù)據(jù)淘汰策略:
noeviction:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),新寫(xiě)入操作會(huì)報(bào)錯(cuò)。 allkeys-lru:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),,移除最近最少使用的 Key。 allkeys-random:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),隨機(jī)移除某個(gè) Key。 volatile-lru:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),在設(shè)置了過(guò)期時(shí)間的鍵空間中,移除最近最少使用的 Key。 volatile-random:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),在設(shè)置了過(guò)期時(shí)間的鍵空間中,隨機(jī)移除某個(gè) Key。 volatile-ttl:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),在設(shè)置了過(guò)期時(shí)間的鍵空間中,有更早過(guò)期時(shí)間的 Key 優(yōu)先移除。
值得一提的是,這里的lru和平常我們所熟知的lru還不完全一樣,redis使用的是采樣概率的思想,省略了雙向鏈表的內(nèi)存消耗。
Redis 會(huì)在每一次處理命令的時(shí)候判斷是否達(dá)到了最大限制,如果達(dá)到則使用對(duì)應(yīng)的算法去刪除涉及到的Key,這時(shí),我們前面所維護(hù)過(guò)鍵的LRU值就會(huì)派上用場(chǎng)了。
Part2Redis為什么這么靠譜
天有不測(cè)風(fēng)云,服務(wù)器也有趴窩的時(shí)候,Redis這個(gè)基于內(nèi)存的存儲(chǔ)遇到服務(wù)器宕機(jī)該怎么應(yīng)對(duì)呢?
2.1RDB持久化
持久化是一種常見(jiàn)的解決方案,那么,我們首先能想到的最簡(jiǎn)單的持久化方案,就是每隔一段時(shí)間把內(nèi)存里的數(shù)據(jù)保存一次,來(lái)避免絕大部分?jǐn)?shù)據(jù)的丟失。這也是Redis的RDB持久化得思路。
RDB有兩種方式,save和bgsave
save,會(huì)阻塞服務(wù)器的其他操作,直到save執(zhí)行完成,所以,這個(gè)期間的所有命令請(qǐng)求都會(huì)被拒絕。對(duì)客戶(hù)端影響較大。
BGSave,由子進(jìn)程進(jìn)行數(shù)據(jù)保存,期間redis仍然可以繼續(xù)處理客戶(hù)端請(qǐng)求。為了防止競(jìng)爭(zhēng)和沖突,bgsave被設(shè)計(jì)成和save/bgrewriteaof操作互斥。
Redis服務(wù)器默認(rèn)每100毫秒執(zhí)行一次,如果數(shù)據(jù)庫(kù)修改次數(shù)(dirty計(jì)數(shù)器)大于設(shè)置的閾值,并且距離上次執(zhí)行保存的時(shí)間(lastsave屬性)大于設(shè)置的閾值,則執(zhí)行保存操作。
因?yàn)槭墙y(tǒng)一批量的保存操作,rdb文件有二進(jìn)制存儲(chǔ)、結(jié)構(gòu)緊湊、空間消耗少、恢復(fù)速度快等特點(diǎn),在持久化方案上不可或缺。
2.2AOF持久化
然而,因?yàn)閎gsave的周期間隔和保存觸發(fā)條件等原因,在服務(wù)器宕機(jī)時(shí),不可避免的會(huì)丟失一部分最新的數(shù)據(jù)。這就需要一些輔助手段來(lái)做持久化補(bǔ)充。
RDB保存的是鍵值對(duì),而AOF則用來(lái)保存寫(xiě)命令。
為什么AOF保存的是命令,而不是鍵值對(duì)呢?
Coder的技術(shù)之路認(rèn)為,一是因?yàn)閍of刷盤(pán),是在文件事件處理過(guò)程當(dāng)中的,具體位置是在結(jié)束一個(gè)事件循環(huán)之前,調(diào)用追加函數(shù)進(jìn)行,所以,使用請(qǐng)求命令來(lái)存儲(chǔ)更方便;二是如果遇到追加過(guò)程中命令被破壞,也可以通過(guò)redis-check-aof來(lái)恢復(fù)(命令恢復(fù)起來(lái)比較方便)。
AOF刷盤(pán)策略,由于aof追加動(dòng)作是和客戶(hù)端請(qǐng)求處理串行執(zhí)行的,所以每次都刷盤(pán)對(duì)性能影響較大,因此都是先追加到aof_buf緩存區(qū)里,而是否同步到AOF文件中則依賴(lài)always、everysec(默認(rèn))、no的刷盤(pán)配置。想比everysec ,always對(duì)性能影響較大,而no則容易丟失數(shù)據(jù)。
AOF文件重寫(xiě)壓縮,AOF因?yàn)楸4媪苏?qǐng)求命令,自然要比RDB更大,并且隨著程序的運(yùn)行,會(huì)越來(lái)越大,然而,文件中有很多冗余的命令數(shù)據(jù)是可以壓縮的,因?yàn)閷?duì)于某個(gè)鍵值對(duì),某一時(shí)刻只會(huì)有一個(gè)狀態(tài)。

那么,在重寫(xiě)過(guò)程中新產(chǎn)生的操作該怎么辦呢?

2.3Sentinel高可用解決方案
上面兩個(gè)小節(jié),主要是在闡述單機(jī)服務(wù)器的數(shù)據(jù)穩(wěn)定性保障,那么,如果是多機(jī)、多進(jìn)程該怎么來(lái)保障呢?
哨兵的作用:監(jiān)視服務(wù)節(jié)點(diǎn)的健康

當(dāng)主節(jié)點(diǎn)宕機(jī)時(shí),由哨兵感知,并在從節(jié)點(diǎn)中重新選舉主節(jié)點(diǎn):

同時(shí),sentinel還會(huì)監(jiān)視宕機(jī)的master節(jié)點(diǎn),恢復(fù)之后會(huì)將其設(shè)置為從節(jié)點(diǎn)加入集群。
除了主從切換的sentinel方案,還有Cluster集群模式來(lái)保障redis的高可用,用來(lái)解決主從復(fù)制的存儲(chǔ)浪費(fèi)問(wèn)題。
Part3Redis6.x的多線程
之前已經(jīng)闡述過(guò)了單線程模型的整體流程,這里不太贅述。
Redis的多線程模型,不是傳統(tǒng)意義上的多線程并發(fā),而是把socket解析回寫(xiě)的這部分操作并行化,以解決IO上的時(shí)間消耗帶來(lái)的系統(tǒng)瓶頸。

對(duì)客戶(hù)端的任何請(qǐng)求,其實(shí)還是主線程在執(zhí)行,避免了操作相同數(shù)據(jù)時(shí)線程間的競(jìng)爭(zhēng),把io部分并行化,降低了io對(duì)資源的損耗,從而提升了系統(tǒng)的吞吐量。仔細(xì)想來(lái),感覺(jué)和rpc中的異步調(diào)用差不多意思,都是綁定來(lái)源,等待處理完成后給給各來(lái)源返回對(duì)應(yīng)結(jié)果。
Part4Redis最佳實(shí)踐
Redis被當(dāng)做分布式緩存的應(yīng)用場(chǎng)景非常普遍,有關(guān)緩存穿透、緩存擊穿、緩存雪崩、數(shù)據(jù)漂移、緩存踩踏、緩存污染、熱點(diǎn)key等常見(jiàn)問(wèn)題,在上一篇文章 諸多策略,緩存為王中已經(jīng)有了詳細(xì)闡述,這里不再重復(fù)。
這里主要給出一些日常開(kāi)發(fā)中的關(guān)注點(diǎn):
Key的設(shè)計(jì)。盡量控制key的長(zhǎng)度,一是過(guò)長(zhǎng)會(huì)占用較多空間,二是我們知道鍵空間是字典類(lèi)型,即時(shí)本身在查找過(guò)程中很快,過(guò)長(zhǎng)的鍵也會(huì)對(duì)比較判斷時(shí)間有所增加。 批量命令的使用。因?yàn)閞edis操作絕大部分都耗在網(wǎng)絡(luò)傳輸上,將多次傳輸改為一次傳輸,大概率會(huì)提升效果。 value的大小。盡量避免大value,原因同上,value太大會(huì)影響網(wǎng)絡(luò)傳輸效率。比如,之前的一次經(jīng)歷,批量獲取了200個(gè)商品的信息(信息比較多,可以認(rèn)為是大value),發(fā)現(xiàn)很慢,后來(lái)把200拆成了4個(gè)50,并行去調(diào)用,效果提升的比較明顯。這個(gè)問(wèn)題也可以考慮用數(shù)據(jù)壓縮的方式進(jìn)行優(yōu)化 復(fù)雜命令的使用。比如排序、聚合等等操作,應(yīng)該在離線階段就處理完畢,然后再存入緩存,而不是在線使用復(fù)雜命令去計(jì)算。 善用數(shù)據(jù)結(jié)構(gòu)。redis豐富的數(shù)據(jù)結(jié)構(gòu)對(duì)支撐業(yè)務(wù)有天然的優(yōu)勢(shì),比如,之前曾用消息隊(duì)列配合bitmap數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)和維護(hù)商品的多個(gè)狀態(tài)(庫(kù)存、上下架、秒殺、黑白名單等),getbit來(lái)直接判斷該商品是否允許展示。
其實(shí)沒(méi)有什么最佳實(shí)踐,業(yè)務(wù)各有各的不同,都需要在實(shí)踐中研究嘗試,如果大家有非常好的實(shí)際案例,也歡迎補(bǔ)充,歡迎留言交流~
— 【 THE END 】— 本公眾號(hào)全部博文已整理成一個(gè)目錄,請(qǐng)?jiān)诠娞?hào)里回復(fù)「m」獲取! 最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)



