redis單線程如何處理并發(fā)的客戶端,以及為何單線程快?
redis的發(fā)展歷程:
redis單線程和多線程的體現(xiàn):
單線程:主要是指Redis的網(wǎng)絡(luò)IO和鍵值對讀寫是由一個(gè)線程來完成的,Redis在處理客戶端的請求時(shí)包括獲取 (socket 讀)、解析、執(zhí)行、內(nèi)容返回 (socket 寫) 等都由一個(gè)順序串行的主線程處理,這就是所謂的“單線程”。這也是Redis對外提供鍵值存儲(chǔ)服務(wù)的主要流程。
多線程:主要是指Redis的其他功能,如持久化RDB/AOF
、異步刪除、集群數(shù)據(jù)同步等,這些均由額外的線程(獨(dú)立redis命令工作線程)執(zhí)行的。
總結(jié):Redis命令工作線程是單線程的,但對整個(gè)Redis來說,是多線程的。
注意:多線程主要是為了提高CPU的利用率,但redis是基于內(nèi)存操作的,它的性能瓶頸可能是內(nèi)存和網(wǎng)絡(luò)帶寬而非CPU,且多線程會(huì)帶來的數(shù)據(jù)同步問題和切換的開銷,故采用單線程足矣。
redis3.x單線程時(shí)代但性能很快的主要原因:
- 基于內(nèi)存的操作:redis所有數(shù)據(jù)都存在內(nèi)存中,且所有運(yùn)算都是內(nèi)存級別的。
- 數(shù)據(jù)結(jié)構(gòu)簡單:這些簡單的數(shù)據(jù)結(jié)構(gòu)查找和操作的時(shí)間大部分復(fù)雜度都是O(1)。
- 多路復(fù)用和非阻塞IO:IO多路復(fù)用監(jiān)聽socket客戶端連接,即可實(shí)現(xiàn)一個(gè)線程處理多個(gè)請求,避免了IO阻塞操作。
- 避免了上下文切換:單線程模型,避免了不必要的上下文切換和多線程競爭,且不會(huì)導(dǎo)致死鎖問題。
redis4.x開始引入多線程:
主要目的:解決redis3.x單線程原子命令操作,帶來的經(jīng)典的問題:大key刪除造成主線程(工作線程)卡頓的問題。
于是在 Redis 4.0 中就新增了多線程的模塊,主要是為了解決刪除數(shù)據(jù)效率比較低的問題的。
異步操作: |
---|
unlink key |
flushdb async |
flushall async |
把刪除工作交給了后臺(tái)的子線程,達(dá)到“異步惰性刪除”數(shù)據(jù)的目的 |
惰性刪除lazy free
:本質(zhì)是將某些cost(時(shí)間復(fù)雜度,即占用主線程cpu時(shí)間片)較高刪除操作,從redis主線程剝離讓子線程來處理,極大地減少主線阻塞時(shí)間,從而減少刪除導(dǎo)致性能和穩(wěn)定性問題。
redis6/redis7引入多線程IO:
前面提到了,redis是基于內(nèi)存操作的,它主要的性能瓶頸可能是內(nèi)存和網(wǎng)絡(luò)帶寬而非CPU。
Redis的之前的單線程架構(gòu),雖然有些命令操作可以用后臺(tái)線程或子進(jìn)程執(zhí)行(比如數(shù)據(jù)刪除、快照生成、AOF重寫)。隨著網(wǎng)絡(luò)硬件的性能提升,單個(gè)主線程處理網(wǎng)絡(luò)請求(讀寫命令)的速度跟不上底層網(wǎng)絡(luò)硬件的速度,故Redis的性能瓶頸就會(huì)轉(zhuǎn)嫁在網(wǎng)絡(luò)IO的處理上。
Redis6/7采用多個(gè)IO線程來處理網(wǎng)絡(luò)請求,提高網(wǎng)絡(luò)請求的并發(fā)處理,對于讀寫操作命令仍然使用單個(gè)工作線程來處理。
- Redis處理請求時(shí),網(wǎng)絡(luò)處理經(jīng)常是瓶頸,通過多個(gè)IO線程并行處理網(wǎng)絡(luò)操作,可以提升實(shí)例的整體處理性能。
- 使用單線程執(zhí)行命令操作,就不用為了保證Lua腳本、事務(wù)的原子性,額外開發(fā)多線程互斥加鎖機(jī)制了。
從Redis6開始,多 I/O 線程的提升讀寫性能,的主要實(shí)現(xiàn)思路:將主線程的 IO 讀寫任務(wù)拆分給一組獨(dú)立的線程去執(zhí)行,這樣就可以使多個(gè) socket 的讀寫可以并行化了,采用多路 I/O 復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請求(盡量減少網(wǎng)絡(luò)IO的時(shí)間消耗),將最耗時(shí)的“Socket的讀取、請求解析、寫入”單獨(dú)外包出去,剩下的命令執(zhí)行仍然由主線程串行執(zhí)行,并和內(nèi)存的數(shù)據(jù)交互。
總結(jié):redis快的原因是:數(shù)據(jù)結(jié)構(gòu)簡單+內(nèi)存+“IO多路復(fù)用(epoll)+多線程網(wǎng)絡(luò)IO”+“工作線程為單線程”(避免了線程切換和保證線程同步、死鎖等問題的產(chǎn)生)。
- IO多路復(fù)用可保證單個(gè)線程監(jiān)聽多個(gè)文件描述符fd(即多個(gè)客戶端的連接)(避免了每個(gè)客戶端連接創(chuàng)建一個(gè)線程造成的資源浪費(fèi)和性能下降),進(jìn)而提高單線程的網(wǎng)絡(luò)IO的吞吐能力。
- 實(shí)際應(yīng)用中,當(dāng)發(fā)現(xiàn)CPU開銷不大但吞吐量卻沒有提升,則可考慮開啟redis7中的多線程網(wǎng)絡(luò)IO機(jī)制,來提升網(wǎng)絡(luò)IO的吞吐量。
數(shù)據(jù)庫和緩存的一致性:
一致性的解決方案:給緩存設(shè)置過期時(shí)間,定期清理緩存并回寫。
如果數(shù)據(jù)庫寫成功,即使緩存更新失敗但只要到達(dá)過期時(shí)間,則后面的讀請求自然會(huì)從數(shù)據(jù)庫中讀取新值然后回填緩存,從而實(shí)現(xiàn)一致性的目的。
“雙檢加鎖”策略,避免緩存擊穿的問題:
當(dāng)redis中查找數(shù)據(jù)沒有時(shí),加鎖(防止高并發(fā)的請求同時(shí)查找該鍵值,導(dǎo)致緩存擊穿問題)之后(加鎖后,只有一個(gè)線程去查mysql其余的線程在阻塞等待,等該線程回寫數(shù)據(jù)到redis中,則直接查詢r(jià)edis即可)二次查詢還是沒有則查詢mysql并回寫到redis中。
4種更新策略:
-
先更新數(shù)據(jù)庫,再更新緩存:
異常情況一:更新myql成功后,再回寫更新redis失敗,則會(huì)導(dǎo)致在該數(shù)據(jù)過期之前,訪問到的是“臟數(shù)據(jù)”。
異常情況二:并發(fā)對同一個(gè)數(shù)據(jù)改寫,且先更新mysql再更新redis,由于執(zhí)行過程搶占CPU資源,可能會(huì)導(dǎo)致出現(xiàn)redis和mysql的數(shù)據(jù)不一致的問題。
-
先更新緩存,再更新數(shù)據(jù)庫:
異常情況:并發(fā)對同一個(gè)數(shù)據(jù)改寫,且先更新緩存,再更新數(shù)據(jù)庫,由于執(zhí)行過程搶占CPU資源,可能會(huì)導(dǎo)致出現(xiàn)redis和mysql的數(shù)據(jù)不一致的問題。
-
先刪除緩存,再更新數(shù)據(jù)庫:
異常情況:
-
A線程delete掉redis中的數(shù)據(jù),之后給mysql更新數(shù)據(jù),結(jié)果網(wǎng)絡(luò)阻塞,一直沒更新成功。
-
B線程來讀取,先去讀redis里數(shù)據(jù)(已經(jīng)被A線程刪除了,故沒有命中),此時(shí),B會(huì)從mysql獲取舊值并回寫到redis中,導(dǎo)致的問題:剛被A線程刪除的舊數(shù)據(jù)有極大可能又被寫回了。
最終,導(dǎo)致mysql和redis中的數(shù)據(jù)不一致,即redis中的是舊數(shù)據(jù)。
“延遲雙刪”的策略:線程A sleep一段時(shí)間時(shí)間后,再次刪除redis中的該數(shù)據(jù)(刪除操作可以異步完成)。
- 這段sleep的時(shí)間 > 線程B讀取數(shù)據(jù)再寫入緩存的時(shí)間。
- 目的:確保讀請求結(jié)束后,寫請求可以刪除讀請求造成的緩存臟數(shù)據(jù)。
- sleep的時(shí)間如何確定?
- 自定估算讀mysql數(shù)據(jù)和寫redis緩存的時(shí)間。
- 啟動(dòng)后臺(tái)監(jiān)控程序,如
WatchDog
監(jiān)控工程。
無法解決的遺留問題:
- 先刪除緩存值再更新數(shù)據(jù)庫,有可能導(dǎo)致高并發(fā)的請求因緩存缺失而訪問數(shù)據(jù)庫,導(dǎo)致打滿mysql。
- 如果業(yè)務(wù)應(yīng)用中,讀取數(shù)據(jù)庫和寫緩存的時(shí)間不好估算,那延遲雙刪中的sleep時(shí)間就不好設(shè)置。
-
-
先更新數(shù)據(jù)庫,再刪除緩存(推薦使用):
-
如果業(yè)務(wù)層要求必須讀取一致性的數(shù)據(jù),那就需要在更新數(shù)據(jù)庫時(shí),先在Redis緩存客戶端暫停并發(fā)讀請求,等數(shù)據(jù)庫更新完、緩存值刪除后,再讀取數(shù)據(jù),從而保證數(shù)據(jù)一致性,但實(shí)際的生產(chǎn)環(huán)境中不推薦(嚴(yán)重影響并發(fā)性能)。
實(shí)際的生產(chǎn)環(huán)境中,分布式下很難滿足“實(shí)時(shí)一致性”,一般都考慮的是最終的一致性。
-
最終的一致性實(shí)現(xiàn)方案:
- 把要?jiǎng)h除的緩存值/要更新的數(shù)據(jù)庫值暫存到消息隊(duì)列(如Kafka/RabbitMQ等)中。
- 當(dāng)程序沒能成功刪除緩存值/更新數(shù)據(jù)庫值時(shí),可以從消息隊(duì)列中重新讀取這些值,然后再次進(jìn)行刪除或更新。
- 如果能夠成功地刪除或更新(保證了數(shù)據(jù)庫和緩存的數(shù)據(jù)一致性),就將這些值從消息隊(duì)列中去除,否則還需要再次進(jìn)行重試。
- 如果重試超過的一定次數(shù)后還未成功,就需要向業(yè)務(wù)層發(fā)送報(bào)錯(cuò)信息,通知運(yùn)維人員處理。
-
總結(jié):
策略 | 高并發(fā)多線程條件下 | 問題 | 現(xiàn)象 | 解決方案 |
---|---|---|---|---|
先刪除redis緩存,再更新mysql | 無 | 緩存刪除成功但數(shù)據(jù)庫更新失敗 | 程序從數(shù)據(jù)庫中讀到舊值 | 再次更新數(shù)據(jù)庫,重試 |
有 | 緩存刪除成功但數(shù)據(jù)庫更新中…有并發(fā)讀請求 | 并發(fā)請求從數(shù)據(jù)庫讀到舊值并回寫到redis,導(dǎo)致后續(xù)(該鍵值過期前)都是從redis讀取到舊值 | 延遲雙刪 | |
先更新mysql,再刪除redis緩存(推薦,該方式可實(shí)現(xiàn)最終一致性) | 無 | 數(shù)據(jù)庫更新成功,但緩存刪除失敗 | 該鍵值過期前,程序從redis中讀到舊值 | 再次刪除緩存,重試 |
有 | 數(shù)據(jù)庫更新成功但緩存刪除中…有并發(fā)讀請求 | 并發(fā)請求從緩存讀到舊值 | 等待redis刪除完成,這段時(shí)間有數(shù)據(jù)不一致,短暫存在。 |
成熟的工程方案–>監(jiān)聽mysql的變動(dòng),并通知redis:
要保證數(shù)據(jù)庫和redis強(qiáng)一致性是不可能的,肯定有少許時(shí)間的不一致。canal是阿里的一套組件,用來監(jiān)聽mysql master發(fā)送的類似binary log的數(shù)據(jù),然后讓消息者去消費(fèi)。
傳統(tǒng)的MySQL的主從復(fù)制:
MySQL的主從復(fù)制將經(jīng)過如下步驟:
-
當(dāng) master 主服務(wù)器上的數(shù)據(jù)發(fā)生改變時(shí),則將其改變寫入二進(jìn)制事件日志文件 bin log 中;
-
salve 從服務(wù)器會(huì)在一定時(shí)間間隔內(nèi)對 master 主服務(wù)器上的二進(jìn)制日志進(jìn)行探測,探測其是否發(fā)生過改變。
如果探測到 master 主服務(wù)器的二進(jìn)制事件日志發(fā)生了改變,則開始一個(gè) I/O Thread 請求 master 二進(jìn)制事件日志;
-
同時(shí) master 主服務(wù)器為每個(gè) I/O Thread 啟動(dòng)一個(gè)dump Thread,用于向其發(fā)送二進(jìn)制事件日志;
-
slave 從服務(wù)器將接收到的二進(jìn)制事件日志保存至自己本地的中繼日志文件 relay log 中;
-
salve 從服務(wù)器將啟動(dòng) SQL Thread 從中繼日志中讀取二進(jìn)制日志,在本地重放,使得其數(shù)據(jù)和主服務(wù)器保持一致;
-
最后 I/O Thread 和 SQL Thread 將進(jìn)入睡眠狀態(tài),等待下一次被喚醒;
canal的工作原理:
- canal 模擬 MySQL slave 的交互協(xié)議,偽裝自己是 MySQL slave,向 MySQL master 發(fā)送 dump 協(xié)議;
- MySQL master 收到 dump 請求,開始推送 bin log 給 slave,即 canel;
- canel 解析 bin log 對象(原始為 bytes 流)。
詳細(xì)介紹:Canal 采集MySQL binlog日志。
canel的作用:
- 數(shù)據(jù)庫鏡像;
- 數(shù)據(jù)庫實(shí)時(shí)備份;
- 索引構(gòu)建和實(shí)時(shí)維護(hù)(拆分異構(gòu)索引、倒排索引等);
- 業(yè)務(wù)cache的刷新;
- 帶業(yè)務(wù)邏輯的增量數(shù)據(jù)處理;
布隆過濾器:
應(yīng)用場景:
-
解決緩存穿透的問題:當(dāng)查詢一個(gè)數(shù)據(jù)庫也不存在的數(shù)據(jù)時(shí),會(huì)造成每次先redis查不到之后都去訪問數(shù)據(jù)庫。
造成的后果:當(dāng)有大量請求查詢數(shù)據(jù)庫不存在的數(shù)據(jù)時(shí),就會(huì)給數(shù)據(jù)庫帶來壓力,甚至?xí)峡鍞?shù)據(jù)庫。
使用布隆過濾器解決緩存穿透的問題:把已存在數(shù)據(jù)的key存在布隆過濾器中。
-
如果布隆過濾器中不存在該條數(shù)據(jù),則數(shù)據(jù)庫中一定不存在該數(shù)據(jù),可直接返回;(避免了緩存擊穿問題)
-
如果布隆過濾器中已存在(可能數(shù)據(jù)庫也不存在該數(shù)據(jù)),先去查詢緩存redis,沒有再去查詢Mysql數(shù)據(jù)庫。
-
-
黑名單校驗(yàn),識別垃圾郵件:
解決方案:把所有億萬個(gè)黑名單都放在布隆過濾器中,在收到郵件時(shí),判斷郵件地址是否在布隆過濾器中即可(布隆過濾器中沒有該郵件地址,則該地址一定不在黑名單中,即一定不是垃圾郵件)。
-
白名單校驗(yàn),識別出合法用戶;
-
準(zhǔn)確快速的判斷50億個(gè)電話號碼中,是否存在這10萬個(gè)?
原理:
-
底層結(jié)構(gòu):由初值為0的bit數(shù)組和一系列無偏hash函數(shù)(無偏主要是為了分布均勻,盡可能的避免hash沖突)構(gòu)成,用來快速判斷集合中是否存在某個(gè)元素。存在一定的誤判概率。
-
目的:在不保存數(shù)據(jù)信息的前提下,只是在內(nèi)存中做一個(gè)是否存在的標(biāo)記flag。
實(shí)現(xiàn)高效的插入和查詢(不能刪除元素,會(huì)導(dǎo)致誤判率增加(布隆過濾器的每一個(gè) bit 并不是獨(dú)占的,很有可能多個(gè)元素共享了某一位。如果我們直接刪除這一位的話,會(huì)影響其他的元素)),且用極少的內(nèi)存空間,但返回的結(jié)果是不確定的并不完美(判斷不存在時(shí),則一定不存在;判斷為存在時(shí),可能不存在)。
總結(jié):布隆過濾器判斷一個(gè)元素不在一個(gè)集合中,則該元素一定不在該集合中。判斷為存在時(shí),可能不存在。
-
添加key:用所有hash函數(shù)進(jìn)行計(jì)算
int index = hash(key) % bloom_filter_len
,并將這些索引對應(yīng)的位置均置1。查詢key:只要有其中一位是0,就表示這個(gè)key不存在,但如果都是1則不一定存在對應(yīng)的key。
-
誤判:指多個(gè)輸入經(jīng)過哈希后相同的 bit 位被置1了,這樣就無法判斷究竟是哪個(gè)輸入產(chǎn)生的,即誤判的根源在于相同的 bit 位被多次映射且置1。
-
當(dāng)實(shí)際元素?cái)?shù)量超過初始化數(shù)量時(shí),應(yīng)該對布隆過濾器進(jìn)行重建,即重新分配一個(gè)size更大的過濾器并將所有歷史元素批量添加進(jìn)去。
緩存創(chuàng)建的問題:
redis緩存淘汰機(jī)制:
redis內(nèi)存管理:
-
redis配置文件:
maxmemory
參數(shù)設(shè)置了redis占用的最大內(nèi)存,單位是bytes字節(jié)。默認(rèn)情況下,64位操作系統(tǒng)不限制內(nèi)存大小,32位系統(tǒng)最多使用3G(推薦占用物理內(nèi)存的3/4)。
-
通過
config get maxmemory 104857600
和config get maxmemory
命令,來設(shè)置和獲取redis的最大內(nèi)存占用。info memory
,可查看redis的內(nèi)存使用情況。
注意:如果redis中的數(shù)據(jù)未設(shè)置過期時(shí)間,且已到達(dá)maxmemory的內(nèi)存上限,這時(shí)再添加數(shù)據(jù)會(huì)OOM
。為避免這種問題,引入了內(nèi)存淘汰機(jī)制。
三種不同的過期鍵刪除策略:
-
立即刪除:redis不可能時(shí)時(shí)刻刻遍歷所有被設(shè)置了過期時(shí)間的key,而且一直選擇立即刪除的話,會(huì)產(chǎn)生大量的CPU性能消耗,同時(shí)也會(huì)影響數(shù)據(jù)的讀取操作。
總結(jié):用處理器性能換取內(nèi)存空間,即用時(shí)間換空間。
-
惰性刪除(
lazyfree-lazy-eviction=yes
開啟):數(shù)據(jù)到達(dá)過期時(shí)間不做處理,等下次訪問時(shí),發(fā)現(xiàn)已過期則刪除。存在的問題:如果已過期的數(shù)據(jù),一段時(shí)間內(nèi)均未訪問到,則會(huì)白白占用內(nèi)存空間。
總結(jié):對memory不友好,用內(nèi)存空間換取處理器性能,即用空間換時(shí)間。
-
定期(抽檢key)刪除(前兩種策略的折中):每隔一段時(shí)間執(zhí)行一次刪除過期鍵操作,并通過限制刪除操作執(zhí)行時(shí)長和頻率,來減少刪除操作對CPU時(shí)間的影響(服務(wù)器要根據(jù)實(shí)際情況,合理設(shè)置刪除操作的執(zhí)行時(shí)長、頻率)。
周期性輪詢r(jià)edis庫中的時(shí)效性數(shù)據(jù),采用隨機(jī)抽取的策略,利用過期數(shù)據(jù)占比的方式控制刪除頻度:
- CPU性能占用設(shè)置有峰值,檢測頻度可自定義設(shè)置;
- 內(nèi)存壓力不是很大,長期占用內(nèi)存的冷數(shù)據(jù)會(huì)被持續(xù)清理;
總結(jié):周期性抽查內(nèi)存空間(隨機(jī)抽查,重點(diǎn)抽查)。
上述“惰性刪除、定期(抽檢)刪除”中,惰性刪除時(shí)從未被點(diǎn)中使用過,定期刪除時(shí)從未被抽查到,均會(huì)造成大量過期key堆積在內(nèi)存中,導(dǎo)致redis內(nèi)存空間緊張。
redis緩存淘汰策略:
redis配置文件:
- noeviction:不會(huì)驅(qū)逐任何key,表示即使內(nèi)存達(dá)到上限也不進(jìn)行置換,之后所有能引起內(nèi)存增加的命令都會(huì)返回error。
- volatile-lru:對所有設(shè)置了過期時(shí)間的key使用LRU算法,進(jìn)行刪除。
- allkeys-lru:對所有key使用LRU算法進(jìn)行刪除,優(yōu)先刪除最近最不常使用的key。
- volatile-lfu:對所有設(shè)置了過期時(shí)間的key使用LFU算法,進(jìn)行刪除。
- allkeys-lfu:對所有key使用LFU算法,進(jìn)行刪除。
- volatile-random:對所有設(shè)置了過期時(shí)間的key,進(jìn)行隨機(jī)刪除。
- allkeys-random:對所有key,進(jìn)行隨機(jī)刪除。
- volatile-ttl:刪除快過期的key。
LRU和LFU的區(qū)別:
- LRU:最近最少使用頁面置換算法,淘汰最長時(shí)間未被使用的頁面,看頁面最后一次被使用到發(fā)生調(diào)度的時(shí)間長短,首先淘汰最長時(shí)間未被使用的頁面。
- LFU:最近最不常用頁面置換算法,淘汰一定時(shí)期內(nèi)被訪問次數(shù)最少的頁,看一定時(shí)間段內(nèi)頁面被使用的頻率,淘汰一定時(shí)期內(nèi)被訪問次數(shù)最少的頁。
配置性能建議:
- 避免存儲(chǔ)bigkey。
- 開啟惰性刪除,
lazyfree-lazy-eviction=yes
。
redis經(jīng)典的五大類型源碼及底層實(shí)現(xiàn):
底層數(shù)據(jù)結(jié)構(gòu):
-
字符串:Redis會(huì)根據(jù)當(dāng)前值的類型和長度決定使用哪種內(nèi)部編碼實(shí)現(xiàn)。
int:8個(gè)字節(jié)的長整型。
embstr:小于等于44個(gè)字節(jié)的字符串。
raw:大于44個(gè)字節(jié)的字符串。
-
哈希:
ziplist(壓縮列表):當(dāng)哈希類型元素個(gè)數(shù)小于
hash-max-ziplist-entries
配置(默認(rèn)512個(gè))、同時(shí)所有值都小于hash-max-ziplist-value
配置(默認(rèn)64 字節(jié))時(shí),Redis會(huì)使用ziplist作為哈希的內(nèi)部實(shí)現(xiàn),ziplist使用更加緊湊的結(jié)構(gòu)實(shí)現(xiàn)多個(gè)元素的連續(xù)存儲(chǔ),故比hashtable更加節(jié)省內(nèi)存。hashtable(哈希表):當(dāng)哈希類型無法滿足ziplist的條件時(shí),Redis會(huì)使用hashtable作為哈希的內(nèi)部實(shí)現(xiàn),因大量元素會(huì)導(dǎo)致ziplist的讀寫效率下降,而hashtable的讀寫時(shí)間復(fù)雜度為
O(1)
。 -
列表:
ziplist(壓縮列表):當(dāng)列表的元素個(gè)數(shù)小于
list-max-ziplist-entries
配置(默認(rèn)512個(gè))、同時(shí)列表中每個(gè)元素的值都小于list-max-ziplist-value
時(shí)(默認(rèn)64字節(jié)),Redis會(huì)選用ziplist來作為列表的內(nèi)部實(shí)現(xiàn)來減少內(nèi)存的使用。linkedlist(鏈表):當(dāng)列表類型無法滿足ziplist的條件時(shí),Redis會(huì)使用linkedlist作為列表的內(nèi)部實(shí)現(xiàn)。
注意:redis7之后,
quicklist
會(huì)將ziplist
和linkedlist
的結(jié)合,即以ziplist為節(jié)點(diǎn)的雙向鏈表linkedlist。 -
集合:
intset(整數(shù)集合):當(dāng)集合中的元素都是整數(shù)且元素個(gè)數(shù)小于
set-max-intset-entries
配置(默認(rèn)512個(gè))時(shí),Redis會(huì)用intset來作為集合的內(nèi)部實(shí)現(xiàn),從而減少內(nèi)存的使用。hashtable(哈希表):當(dāng)集合類型無法滿足intset的條件時(shí),Redis會(huì)使用hashtable作為集合的內(nèi)部實(shí)現(xiàn)。
-
有序集合:
ziplist(壓縮列表):當(dāng)有序集合的元素個(gè)數(shù)小于
zset-max-ziplist- entries
配置(默認(rèn)128個(gè)),同時(shí)每個(gè)元素的值都小于zset-max-ziplist-value
配置(默認(rèn)64字節(jié))時(shí),Redis會(huì)用ziplist來作為有序集合的內(nèi)部實(shí)現(xiàn),從而有效的減少內(nèi)存使用。skiplist(跳躍表):當(dāng)ziplist條件不滿足時(shí),有序集合會(huì)使用skiplist作為內(nèi)部實(shí)現(xiàn),因?yàn)榇藭r(shí)ziplist的讀寫效率會(huì)下降。
注意:redis7會(huì)將ziplist換成了listpack,其通過每個(gè)節(jié)點(diǎn)記錄自己的長度且放在節(jié)點(diǎn)的尾部,徹底解決掉了 ziplist 存在的連鎖更新的問題。
redis字典數(shù)據(jù)庫kv鍵值對是什么?
怎么實(shí)現(xiàn)的鍵值對數(shù)據(jù)庫?
key:一般都是String類型的字符串對象。
value:是redis對象,如字符串對象,集合類型的對象:list對象,set對象,hash對象,zset對象。
注意:每個(gè)鍵值對,都會(huì)有一個(gè)dictEntry。redis中,每個(gè)對象都是一個(gè)redisObject結(jié)構(gòu)體,redisObject中定義了type和encoding字段對不同的數(shù)據(jù)類型加以區(qū)別,即簡單來說:redisObject就是string、hash、list、set、zset的父類。
string數(shù)據(jù)結(jié)構(gòu):
SDS簡單動(dòng)態(tài)字符串:
采用len記錄字符串的長度,比起c語言用\0
作為字符串的終止判斷更安全。采用預(yù)分配alloc的機(jī)制,大大降低了內(nèi)存碎片的產(chǎn)生。
注意:embstr
和raw
類型的底層數(shù)據(jù)結(jié)構(gòu),就是SDS(簡單動(dòng)態(tài)字符串)。
底層編碼方式 | 細(xì)節(jié)說明 |
---|---|
int | Long類型整數(shù)時(shí),RedisObject中的ptr指針直接賦值為整數(shù)數(shù)據(jù),不再額外的指針再指向整數(shù)了,節(jié)省了指針的空間開銷。 |
embstr | 當(dāng)待保存的字符串小于等于44字節(jié)時(shí),embstr類型將會(huì)調(diào)用內(nèi)存分配函數(shù),只分配一塊連續(xù)的內(nèi)存空間,空間中依次包含 redisObject 與 sdshdr 兩個(gè)數(shù)據(jù)結(jié)構(gòu),讓元數(shù)據(jù)、指針和SDS是一塊連續(xù)內(nèi)存區(qū)域,避免產(chǎn)生內(nèi)存碎片。 |
raw | 當(dāng)待保存的字符串大于44字節(jié)時(shí),SDS的數(shù)據(jù)量變多變大了,會(huì)給SDS分配多的空間并用指針指向SDS結(jié)構(gòu),raw 類型將會(huì)調(diào)用兩次內(nèi)存分配函數(shù),分配兩塊內(nèi)存空間,一塊用于包含 redisObject 結(jié)構(gòu),而另一塊用于包含 sdshdr 結(jié)構(gòu)。 |
3大物理編碼方式:
int:
保存long型(長整型)的64位(8字節(jié))有符號整數(shù)。
注意:int最多是19位,且浮點(diǎn)數(shù)在redis內(nèi)部會(huì)用字符串保存。
embstr:
代表embstr格式的SDS(Simple Dynamic String)簡單動(dòng)態(tài)字符串,保存長度小于44字節(jié)的字符串。
優(yōu)點(diǎn):使用連續(xù)的內(nèi)存空間存儲(chǔ),避免了內(nèi)存碎片的產(chǎn)生。
raw:
保存長度大于44字節(jié)的字符串。
三種編碼的轉(zhuǎn)變邏輯:
hash數(shù)據(jù)結(jié)構(gòu):
hash的兩種編碼:redis6 --> ziplist + hashtable
、redis7 --> listpack + hashtable
。
redis6:
hash-max-ziplist-entries
:使用壓縮列表保存時(shí),哈希集合中的最大元素個(gè)數(shù)。
hash-max-ziplist-value
:使用壓縮列表保存時(shí),哈希集合中單個(gè)元素的最大長度。
Hash類型鍵的字段個(gè)數(shù) 小于hash-max-ziplist-entries
并且每個(gè)字段名和字段值的長度 小于hash-max-ziplist-value
時(shí),Redis才會(huì)使用OBJ_ENCODING_ZIPLIST
來存儲(chǔ)該鍵,任意一個(gè)不滿足則會(huì)轉(zhuǎn)換為OBJ_ENCODING_HT
的編碼方式。
一旦底層編碼從壓縮列表OBJ_ENCODING_ZIPLIST
轉(zhuǎn)為了哈希表OBJ_ENCODING_HT
,Hash類型就會(huì)一直用哈希表進(jìn)行保存而不會(huì)再轉(zhuǎn)回壓縮列表了,但在節(jié)省內(nèi)存空間方面哈希表就沒有壓縮列表高效了。
OBJ_ENCODING_HT
這種編碼方式內(nèi)部才是真正的哈希表結(jié)構(gòu),稱為字典結(jié)構(gòu),其可以實(shí)現(xiàn)O(1)
復(fù)雜度的讀寫操作,因此效率很高。從OBJ_ENCODING_HT
類型到底層真正的散列表數(shù)據(jù)結(jié)構(gòu)是一層層嵌套下去的,組織關(guān)系見面圖:
側(cè)面證明:每個(gè)鍵值對都有一個(gè)dictEntry節(jié)點(diǎn)。
ziplist
壓縮列表,是一種緊湊的連續(xù)存儲(chǔ)的“雙向鏈表”編碼格式(每個(gè)節(jié)點(diǎn)不再存儲(chǔ)prev、next指針,而是存儲(chǔ)上一個(gè)節(jié)點(diǎn)和當(dāng)前節(jié)點(diǎn)的長度),以部分的讀寫性能為代價(jià),換取極高的內(nèi)存空間利用率。 一般適用于字段個(gè)數(shù)少,字段值也比較小的場景。
最主要的是節(jié)點(diǎn)zlentry中,包含了前一個(gè)節(jié)點(diǎn)的長度prevrawlensize
、當(dāng)前節(jié)點(diǎn)的長度lensize
、當(dāng)前節(jié)點(diǎn)的類型encoding
和當(dāng)前節(jié)點(diǎn)的實(shí)際數(shù)據(jù)p
。
總結(jié):
- ziplist為了節(jié)省內(nèi)存,采用緊湊的連續(xù)存儲(chǔ)格式。
- ziplist作為一個(gè)雙向鏈表,可在時(shí)間復(fù)雜度為O(1)下,從頭部、尾部進(jìn)行pop、push,即頭插尾插效率高。
- 致命缺點(diǎn):插入/更新元素時(shí),可能會(huì)出現(xiàn)連鎖更新現(xiàn)象,導(dǎo)致其被listpack替換。
- 一般適用于字段個(gè)數(shù)少,字段值也比較小的場景。過多的字段,會(huì)導(dǎo)致查詢/插入效率的降低。
redis7:
hash-max-listpack-entries
:使用壓縮列表保存時(shí),哈希集合中的最大元素個(gè)數(shù)。
hash-max-listpack-value
:使用壓縮列表保存時(shí),哈希集合中單個(gè)元素的最大長度。
Hash類型鍵的字段個(gè)數(shù) 小于hash-max-listpack-entries
并且每個(gè)字段名和字段值的長度 小于hash-max-listpack-value
時(shí),Redis才會(huì)使用OBJ_ENCODING_LISTPACK
來存儲(chǔ)該鍵,任意一個(gè)不滿足則會(huì)轉(zhuǎn)換為OBJ_ENCODING_HT
的編碼方式。
有了ziplist,為何還需要listpack緊湊列表?
-
ziplist壓縮列表的每個(gè)節(jié)點(diǎn)中 prevlen 屬性,記錄前一個(gè)節(jié)點(diǎn)占用的長度,
- 如果前一個(gè)節(jié)點(diǎn)的長度 < 254bytes,則 prevlen 屬性需要占用 1bytes 來存儲(chǔ)前一個(gè)節(jié)點(diǎn)的長度值。
- 如果前一個(gè)節(jié)點(diǎn)的長度 >= 254bytes,則 prevlen 屬性需要占用 5bytes 來存儲(chǔ)前一個(gè)節(jié)點(diǎn)的長度值。
-
壓縮列表新增某個(gè)元素或修改某個(gè)元素時(shí),如果空間不夠,壓縮列表占用的內(nèi)存空間就需要重新分配。
當(dāng)新插入的元素較大時(shí),可能會(huì)導(dǎo)致后續(xù)元素的 prevlen 占用空間都發(fā)生變化,從而引起連鎖更新問題,導(dǎo)致每個(gè)元素的空間都要重新分配,造成訪問壓縮列表性能下降。
-
listpack 是 Redis 設(shè)計(jì)用來取代掉 ziplist 的數(shù)據(jù)結(jié)構(gòu),它通過每個(gè)節(jié)點(diǎn)記錄自己的長度且放在節(jié)點(diǎn)的尾部,來徹底解決掉了 ziplist 存在的連鎖更新的問題。
list數(shù)據(jù)結(jié)構(gòu):
ziplist壓縮列表和linkedlist雙端鏈表的對比:
- ziplist的優(yōu)點(diǎn):內(nèi)存緊湊,訪問效率高;缺點(diǎn):更新效率低,可能引發(fā)“連鎖更新”的問題。
- linkedlist的優(yōu)點(diǎn):節(jié)點(diǎn)的修改效率高;缺點(diǎn):需要額外的內(nèi)存開銷,節(jié)點(diǎn)較多時(shí)會(huì)產(chǎn)生大量的內(nèi)存碎片。
總結(jié):quicklist
結(jié)合了ziplist
和linkedlist
的優(yōu)點(diǎn),是“雙向鏈表 + 壓縮列表”組合:鏈表中每個(gè)元素是一個(gè)壓縮列表。
list中quicklist的兩種編碼:redis6 --> linkedlist + ziplist
、redis7 --> linkedlist + listpack
。
redis6:
- ziplist 壓縮配置
list-compress-depth
,表示一個(gè) quicklist 兩端不被壓縮的節(jié)點(diǎn)個(gè)數(shù)。這里的節(jié)點(diǎn)是指 quicklist 雙向鏈表的節(jié)點(diǎn),而非 ziplist 里面的數(shù)據(jù)項(xiàng)個(gè)數(shù)。取值含義如下:
-
0
:是 Redis 的默認(rèn)值,表示都不壓縮。 -
1
:quicklist 兩端各有1個(gè)節(jié)點(diǎn)不壓縮,中間的節(jié)點(diǎn)壓縮。 -
2
:quicklist 兩端各有2個(gè)節(jié)點(diǎn)不壓縮,中間的節(jié)點(diǎn)壓縮。 -
3
:quicklist 兩端各有3個(gè)節(jié)點(diǎn)不壓縮,中間的節(jié)點(diǎn)壓縮。
- ziplist 中 entry 配置
list-max-ziplist-size
-
當(dāng)取正值時(shí),表示按照數(shù)據(jù)項(xiàng)個(gè)數(shù)來限定每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 長度。
比如,當(dāng)這個(gè)參數(shù)配置成5的時(shí)候,表示每個(gè) quicklist 節(jié)點(diǎn)的 ziplist 最多包含5個(gè)數(shù)據(jù)項(xiàng)。
-
當(dāng)取負(fù)值時(shí),表示按照占用字節(jié)數(shù)來限定每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 長度。這時(shí),它只能取 -1 到 -5 這五個(gè)值。
取值含義如下:
-5
:每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 大小不能超過64 Kb。(注:1kb => 1024 bytes)-4
:每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 大小不能超過32 Kb。-3
:每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 大小不能超過16 Kb。-2
(Redis的默認(rèn)值):每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 大小不能超過8 Kb。-1
:每個(gè) quicklist 節(jié)點(diǎn)上的 ziplist 大小不能超過4 Kb。
redis7:
主要與redis6的區(qū)別(listpack 替換了 ziplist):即通過每個(gè)節(jié)點(diǎn)記錄自己的長度且放在節(jié)點(diǎn)的尾部,來徹底解決掉了 ziplist 存在的連鎖更新的問題。
set數(shù)據(jù)結(jié)構(gòu):
兩種編碼格式:intset
、hashtable
。
zset數(shù)據(jù)結(jié)構(gòu):
編碼格式:redis6 --> ziplist + skiplist
、redis7 --> listpack + skiplist
。
- 當(dāng)有序集合中包含的元素?cái)?shù)量超過服務(wù)器屬性
zset_max_ziplist_entries
的值(默認(rèn)值為 128 )時(shí) - 有序集合中新添加元素的長度大于服務(wù)器屬性
zset_max_ziplist_value
的值(默認(rèn)值為 64 )時(shí)
,redis會(huì)使用跳躍表 skiplist
作為有序集合的底層實(shí)現(xiàn),否則會(huì)使用 ziplist/listpack
。
skiplist跳表:
引出:
對于一個(gè)單鏈表來講,即便鏈表中存儲(chǔ)的數(shù)據(jù)是有序的,如果要想在其中查找某個(gè)數(shù)據(jù),也只能從頭到尾遍歷鏈表,時(shí)間復(fù)雜度很高O(N)。
解決方法:升維,即最典型的空間換時(shí)間解決方案。實(shí)現(xiàn)細(xì)節(jié)問題:
- 鏈表無法進(jìn)行二分查找,故借鑒數(shù)據(jù)庫索引的思想,提取出鏈表中關(guān)鍵節(jié)點(diǎn)(作為索引),先在關(guān)鍵節(jié)點(diǎn)上查找,再進(jìn)入下層鏈表查找,提取多層關(guān)鍵節(jié)點(diǎn),就形成了跳躍表。
- 由于索引也要占據(jù)一定空間的,所以,索引添加的越多,空間占用的越多。
特性:
- skiplist就是實(shí)現(xiàn)二分查找的有序鏈表,即“跳表=鏈表+多級索引”。
- 跳表的時(shí)間復(fù)雜度
O(logN)
,空間復(fù)雜度O(N)
。
適用場景:
只有在**數(shù)據(jù)量較大且讀多寫少**的情況下,才能體現(xiàn)出來優(yōu)勢。文章來源:http://www.zghlxwxcb.cn/news/detail-678493.html
缺點(diǎn):
維護(hù)成本相對要高,新增/刪除元素時(shí)需要把所有索引都更新一遍,且為了保證原始鏈表中數(shù)據(jù)的有序性需要先找到要?jiǎng)幼鞯奈恢?,之后才能插?刪除并更新索引。文章來源地址http://www.zghlxwxcb.cn/news/detail-678493.html
到了這里,關(guān)于redis學(xué)習(xí)筆記 - 進(jìn)階部分的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!