深入理解redis原理!
原理篇
redis 時(shí)單線(xiàn)程的為什么還能那么快?
數(shù)據(jù)都在內(nèi)存中,運(yùn)算都是內(nèi)存級(jí)別的運(yùn)算。
redis既然是單線(xiàn)程的為什么能處理那么多的并發(fā)數(shù)?
多路復(fù)用,操作系統(tǒng)時(shí)間輪訓(xùn)epoll 函數(shù)作為選擇器,維護(hù)了指令隊(duì)列,和響應(yīng)隊(duì)列,java的nio。
select ,poll, epoll
rset ,fds(文件描述符的集合)。而select模型存儲(chǔ)fds的方式是采取的bitmap,默認(rèn)最大1024個(gè)。
2.3、執(zhí)行流程
1.select模型每次都直接將rset(也就是fds)全部拷貝到內(nèi)核態(tài),因?yàn)閮?nèi)核態(tài)速度比用戶(hù)空間態(tài)快很多。
2.如果沒(méi)數(shù)據(jù)的話(huà),select函數(shù)會(huì)阻塞,如果有數(shù)據(jù)的話(huà)會(huì)執(zhí)行兩步
(1)將有數(shù)據(jù)的那個(gè)fd置位(也就是標(biāo)記一下,代表這個(gè)fd有數(shù)據(jù))
(2)select函數(shù)不在阻塞,將繼續(xù)往下執(zhí)行。也就是整體遍歷fds,找到有數(shù)據(jù)的那個(gè)fd讀取數(shù)據(jù)做處理。他的fd不能重用,每一次都需要重新創(chuàng)建新的fds且將用戶(hù)空間態(tài)的fds拷貝到內(nèi)核態(tài)
3、缺點(diǎn)
fds最大支持1024個(gè)(可以更改,但是意義不大)
fd不可重用,每次內(nèi)核態(tài)都給置位了,導(dǎo)致為了標(biāo)記fd,必須創(chuàng)建一個(gè)新的rset從而導(dǎo)致fds在用戶(hù)態(tài)內(nèi)存態(tài)間多次拷貝(也就是fds)
用戶(hù)控件態(tài)拷貝rset到內(nèi)核態(tài)也需要時(shí)間,雖然內(nèi)核態(tài)執(zhí)行比用戶(hù)態(tài)快,但是copy也需要開(kāi)銷(xiāo)
O(n)再次遍歷問(wèn)題。因?yàn)閞set里的fd被置位后,select函數(shù)并不知道哪個(gè)被置位了,需要從頭遍歷到尾,逐個(gè)對(duì)比。
poll
poll的結(jié)構(gòu)體是為了fd重復(fù)利用,不需要每次都拷貝到內(nèi)核態(tài)用的。
1、解決了select哪些問(wèn)題
采取的鏈表存儲(chǔ),而不是bitmap,解決了1024長(zhǎng)度限制問(wèn)題
采取結(jié)構(gòu)體每次置位結(jié)構(gòu)體內(nèi)的revents字段,而不破壞fd本身,所以可重用,不需要每次都創(chuàng)建新的fd。
2、缺點(diǎn)
用戶(hù)控件態(tài)拷貝rset到內(nèi)核態(tài)也需要時(shí)間,雖然內(nèi)核態(tài)執(zhí)行比用戶(hù)態(tài)快,但是copy也需要開(kāi)銷(xiāo)
O(n)再次遍歷問(wèn)題。因?yàn)閞set里的fd被置位后,select函數(shù)并不知道哪個(gè)被置位了,需要從頭遍歷到尾,逐個(gè)對(duì)比。
epoll
2.2、執(zhí)行流程
epoll將fd放到了紅黑樹(shù)里,且不需要拷貝到內(nèi)核態(tài),因?yàn)樗扇×恕肮蚕韮?nèi)存”的概念。(其實(shí)還是復(fù)制,只是復(fù)制采取了其他技術(shù)可以使開(kāi)銷(xiāo)極其的小)
epoll的置位是重排,比如五個(gè)fd, 1 2 3 4 5,1 3 5這三個(gè)fd有數(shù)據(jù)了,那么他會(huì)重排序,排成如下1 3 5 2 4。(也有的說(shuō)是單獨(dú)放到新的數(shù)組里)
每一次置位nfds的值都+1。且會(huì)回調(diào)epoll_wait
所以epoll_wait執(zhí)行完會(huì)返回有幾個(gè)fd有數(shù)據(jù),那么下面的for直接遍歷nfds次即可。解決了前面的兩種O(n)。變成了O1
總結(jié)如下:
比如三個(gè)redis-cli,假設(shè)2個(gè)redis-cli寫(xiě)入命令,
select:那么select模型是輪詢(xún)這三個(gè)redis-cli的fd,看哪個(gè)fd有消息,有的話(huà)讀取處理消息。當(dāng)他下次再寫(xiě)命令的時(shí)候還需要重新創(chuàng)建fd,然后復(fù)制到內(nèi)核態(tài)然后再遍歷全部。
poll:那么poll模型是輪詢(xún)這三個(gè)redis-cli的fd,看哪個(gè)fd有消息,有的話(huà)讀取處理消息。下次再寫(xiě)入的時(shí)候還是遍歷全局fd,看哪個(gè)fd有消息進(jìn)行處理。省去了每次都創(chuàng)建新的fd且復(fù)制的過(guò)程。
epoll:epoll就不輪詢(xún)了,有消息進(jìn)來(lái)后你通知我,我去處理你的消息,那些沒(méi)消息的fd我不管。而且復(fù)制到內(nèi)核態(tài)的過(guò)程我采取牛逼的技術(shù)讓開(kāi)銷(xiāo)達(dá)到最小的極致。
原文鏈接:https://blog.csdn.net/ctwctw/java/article/details/105024324
Redis 服務(wù)器與客戶(hù)端通過(guò) RESP(REdis Serialization Protocol) 協(xié)議通信。
主要以下特點(diǎn): 容易實(shí)現(xiàn),解析快,人類(lèi)可讀.
RESP 底層采用的是 TCP 的連接方式, 通過(guò) tcp 進(jìn)行數(shù)據(jù)傳輸, 然后根據(jù)解析規(guī)則解析相
應(yīng)信息, 完成交互。
持久化
(原文鏈接:https://blog.csdn.net/ctwctw/java/article/details/105147277)
一、為什么需要持久化
redis里有10gb數(shù)據(jù),突然停電或者意外宕機(jī)了,再啟動(dòng)的時(shí)候10gb都沒(méi)了?!所以需要持久化,宕機(jī)后再通過(guò)持久化文件將數(shù)據(jù)恢復(fù)。
二、優(yōu)缺點(diǎn)
1、rdb文件
rdb文件都是二進(jìn)制,很小。比如內(nèi)存數(shù)據(jù)有10gb,rdb文件可能就1gb,只是舉例。
2、優(yōu)點(diǎn)
由于rdb文件都是二進(jìn)制文件,所以很小,在災(zāi)難恢復(fù)的時(shí)候會(huì)快些。
他的效率(主進(jìn)程處理命令的效率,而不是持久化的效率)相對(duì)于aof要高(bgsave而不是save),因?yàn)槊縼?lái)個(gè)請(qǐng)求他都不會(huì)處理任何事,只是bgsave的時(shí)候他會(huì)fork()子進(jìn)程且可能copyonwrite,但copyonwrite只是一個(gè)尋址的過(guò)程,納秒級(jí)別的。而aof每次都是寫(xiě)盤(pán)操作,毫米級(jí)別。沒(méi)法比。
3、缺點(diǎn)
數(shù)據(jù)可靠性比aof低,也就是會(huì)丟失的多。因?yàn)閍of可以配置每秒都持久化或者每個(gè)命令處理完就持久化一次這種高頻率的操作,而rdb的話(huà)雖然也是靠配置進(jìn)行bgsave,但是沒(méi)有aof配置那么靈活,也沒(méi)aof持久化快,因?yàn)閞db每次全量,aof每次只追加。
三、RDB持久化的兩種方法
配置文件也可以配置觸發(fā)rdb的規(guī)則。配置文件配置的規(guī)則采取的是bgsave的原理。
1、save
1.1、描述 aof 處理增量數(shù)據(jù)
同步、阻塞
1.2、缺點(diǎn)
致命的問(wèn)題,持久化的時(shí)候redis服務(wù)阻塞(準(zhǔn)確的說(shuō)會(huì)阻塞當(dāng)前執(zhí)行save命令的線(xiàn)程,但是redis是單線(xiàn)程的,所以整個(gè)服務(wù)會(huì)阻塞),不能繼對(duì)外提供請(qǐng)求,GG!數(shù)據(jù)量小的話(huà)肯定影響不大,數(shù)據(jù)量大呢?每次復(fù)制需要1小時(shí),那就相當(dāng)于停機(jī)一小時(shí)。
2、bgsave
2.1、描述 rdb 處理全量數(shù)據(jù)
異步、非阻塞
2.2、原理
fork() + copyonwrite
2.3、優(yōu)點(diǎn)
他可以一邊進(jìn)行持久化,一邊對(duì)外提供讀寫(xiě)服務(wù),互不影響,新寫(xiě)的數(shù)據(jù)對(duì)我持久化不會(huì)造成數(shù)據(jù)影響,你持久化的過(guò)程中報(bào)錯(cuò)或者耗時(shí)太久都對(duì)我當(dāng)前對(duì)外提供請(qǐng)求的服務(wù)不會(huì)產(chǎn)生任何影響。持久化完會(huì)將新的rdb文件覆蓋之前的。
四 、fork()
bgsave原理是fork() + copyonwrite,那么現(xiàn)在來(lái)聊一下fork()
1、fork()是什么
fork()是unix和linux這種操作系統(tǒng)的一個(gè)api,而不是Redis的api。
2、fork()有什么用
fork()用于創(chuàng)建一個(gè)子進(jìn)程,注意是子進(jìn)程,不是子線(xiàn)程。fork()出來(lái)的進(jìn)程共享其父類(lèi)的內(nèi)存數(shù)據(jù)。僅僅是共享fork()出子進(jìn)程的那一刻的內(nèi)存數(shù)據(jù),后期主進(jìn)程修改數(shù)據(jù)對(duì)子進(jìn)程不可見(jiàn),同理,子進(jìn)程修改的數(shù)據(jù)對(duì)主進(jìn)程也不可見(jiàn)。比如:A進(jìn)程fork()了一個(gè)子進(jìn)程B,那么A進(jìn)程就稱(chēng)之為主進(jìn)程,這時(shí)候主進(jìn)程子進(jìn)程所指向的內(nèi)存空間是同一個(gè),所以他們的數(shù)據(jù)一致。但是A修改了內(nèi)存上的一條數(shù)據(jù),這時(shí)候B是看不到的,A新增一條數(shù)據(jù),刪除一條數(shù)據(jù),B都是看不到的。而且子進(jìn)程B出問(wèn)題了,對(duì)我主進(jìn)程A完全沒(méi)影響,我依然可以對(duì)外提供服務(wù),但是主進(jìn)程掛了,子進(jìn)程也必須跟隨一起掛。這一點(diǎn)有點(diǎn)像守護(hù)線(xiàn)程的概念。Redis正是巧妙的運(yùn)用了fork()這個(gè)牛逼的api來(lái)完成RDB的持久化操作。
五、Redis中的fork()
Redis巧妙的運(yùn)用了fork()。當(dāng)bgsave執(zhí)行時(shí),Redis主進(jìn)程會(huì)判斷當(dāng)前是否有fork()出來(lái)的子進(jìn)程,若有則忽略,若沒(méi)有則會(huì)fork()出一個(gè)子進(jìn)程來(lái)執(zhí)行rdb文件持久化的工作,子進(jìn)程與Redis主進(jìn)程共享同一份內(nèi)存空間,所以子進(jìn)程可以搞他的rdb文件持久化工作,主進(jìn)程又能繼續(xù)他的對(duì)外提供服務(wù),二者互不影響。我們說(shuō)了他們之后的修改內(nèi)存數(shù)據(jù)對(duì)彼此不可見(jiàn),但是明明指向的都是同一塊內(nèi)存空間,這是咋搞得?肯定不可能是fork()出來(lái)子進(jìn)程后順帶復(fù)制了一份數(shù)據(jù)出來(lái),如果是這樣的話(huà)比如我有4g內(nèi)存,那么其實(shí)最大有限空間是2g,我要給rdb留出一半空間來(lái),扯淡一樣!那他咋做的?采取了copyonwrite技術(shù)。
六、copyonwrite
很簡(jiǎn)單,現(xiàn)在不就是主進(jìn)程和子進(jìn)程共享了一塊內(nèi)存空間,怎么做到的彼此更改互不影響嗎?
1、原理
主進(jìn)程fork()子進(jìn)程之后,內(nèi)核把主進(jìn)程中所有的內(nèi)存頁(yè)的權(quán)限都設(shè)為read-only,然后子進(jìn)程的地址空間指向主進(jìn)程。這也就是共享了主進(jìn)程的內(nèi)存,當(dāng)其中某個(gè)進(jìn)程寫(xiě)內(nèi)存時(shí)(這里肯定是主進(jìn)程寫(xiě),因?yàn)樽舆M(jìn)程只負(fù)責(zé)rdb文件持久化工作,不參與客戶(hù)端的請(qǐng)求),CPU硬件檢測(cè)到內(nèi)存頁(yè)是read-only的,于是觸發(fā)頁(yè)異常中斷(page-fault),陷入內(nèi)核的一個(gè)中斷例程。中斷例程中,內(nèi)核就會(huì)把觸發(fā)的異常的頁(yè)復(fù)制一份(這里僅僅復(fù)制異常頁(yè),也就是所修改的那個(gè)數(shù)據(jù)頁(yè),而不是內(nèi)存中的全部數(shù)據(jù)),于是主子進(jìn)程各自持有獨(dú)立的一份。
數(shù)據(jù)修改之前的樣子
數(shù)據(jù)修改之后的樣子
2、回到原問(wèn)題
其實(shí)就是更改數(shù)據(jù)的之前進(jìn)行copy一份更改數(shù)據(jù)的數(shù)據(jù)頁(yè)出來(lái),比如主進(jìn)程收到了set k 1請(qǐng)求(之前k的值是2),然后這同時(shí)又有子進(jìn)程在rdb持久化,那么主進(jìn)程就會(huì)把k這個(gè)key的數(shù)據(jù)頁(yè)拷貝一份,并且主進(jìn)程中k這個(gè)指針指向新拷貝出來(lái)的數(shù)據(jù)頁(yè)地址上,然后進(jìn)行更改值為1的操作,這個(gè)主進(jìn)程k元素地址引用的新拷貝出來(lái)的地址,而子進(jìn)程引用的內(nèi)存數(shù)據(jù)k還是修改之前的。
3、一段話(huà)總結(jié)
copyonwritefork()出來(lái)的子進(jìn)程共享主進(jìn)程的物理空間,當(dāng)主子進(jìn)程有內(nèi)存寫(xiě)入操作時(shí),read-only內(nèi)存頁(yè)發(fā)生中斷,將觸發(fā)的異常的內(nèi)存頁(yè)復(fù)制一份(其余的頁(yè)還是共享主進(jìn)程的)。
4、額外補(bǔ)充
在 Redis 服務(wù)中,子進(jìn)程只會(huì)讀取共享內(nèi)存中的數(shù)據(jù),它并不會(huì)執(zhí)行任何寫(xiě)操作,只有主進(jìn)程會(huì)在寫(xiě)入時(shí)才會(huì)觸發(fā)這一機(jī)制,而對(duì)于大多數(shù)的 Redis 服務(wù)或者數(shù)據(jù)庫(kù),寫(xiě)請(qǐng)求往往都是遠(yuǎn)小于讀請(qǐng)求的,所以使用fork()加上寫(xiě)時(shí)拷貝這一機(jī)制能夠帶來(lái)非常好的性能,也讓BGSAVE這一操作的實(shí)現(xiàn)變得很簡(jiǎn)單。
七、疑問(wèn)
0、調(diào)用fork()也會(huì)阻塞啊
我只能說(shuō)沒(méi)毛病,但是這個(gè)阻塞真的可以忽略不計(jì)。尤其是相對(duì)于阻塞主線(xiàn)程的save。
1、會(huì)同時(shí)存在多個(gè)子進(jìn)程嗎?
不會(huì),主進(jìn)程每次收到bgsave命令需要fork()子進(jìn)程之前都會(huì)判斷是否存在子進(jìn)程了,若存在也會(huì)忽略掉這次bgsave請(qǐng)求。若不存在我會(huì)fork()出子進(jìn)程進(jìn)行工作。
為什么這么搞?
我猜測(cè)原因如下:
1.如果支持并行存在多個(gè)子進(jìn)程,那么不僅會(huì)拉低服務(wù)器性能,還會(huì)造成數(shù)據(jù)問(wèn)題,比如八點(diǎn)的bgsave在工作,九點(diǎn)又來(lái)個(gè)bgsave命令。這時(shí)候九點(diǎn)的先執(zhí)行完了,八點(diǎn)的后執(zhí)行完了,那九點(diǎn)的不白執(zhí)行了嗎?這是我所謂的數(shù)據(jù)問(wèn)題。再比如,都沒(méi)執(zhí)行完,十點(diǎn)又開(kāi)一個(gè)bgsave,越積越多,服務(wù)器性能被拉低。
2.那為什么不阻塞?判斷有子進(jìn)程在工作,就等待,等他執(zhí)行完我在上場(chǎng),那一樣,越積越多,文件過(guò)大,只會(huì)造成堆積。
2、如果沒(méi)有copyonwrite這種技術(shù)是什么效果?
1.假設(shè)是全量復(fù)制,那么內(nèi)存空間直接減半,浪費(fèi)資源不說(shuō),數(shù)據(jù)量10g,全量復(fù)制這10g的時(shí)間也夠長(zhǎng)的。這誰(shuí)頂?shù)米。?.如果不全量復(fù)制,會(huì)是怎樣?相當(dāng)于我一邊復(fù)制,你一邊寫(xiě)數(shù)據(jù),看著貌似問(wèn)題不大,其實(shí)不然。比如現(xiàn)在Redis里有k1的值是1,k2的值是
2,比如bgsave了,這時(shí)候rdb寫(xiě)入了k1的值,在寫(xiě)k2的值之前時(shí),有個(gè)客戶(hù)端請(qǐng)求
set k1 11
set k2 22
1
2
那么持久化進(jìn)去的是k2 22,但是k1的值還是1,而不是最新的11,所以會(huì)造成數(shù)據(jù)問(wèn)題,所以采取了copyonwrite技術(shù)來(lái)保證觸發(fā)bgsave請(qǐng)求的時(shí)候無(wú)論你怎么更改,都對(duì)我rdb文件的數(shù)據(jù)持久化不會(huì)造成任何影響。
redis cluster集群
redis cluster
redis cluster是Redis的分布式解決方案,在3.0版本推出后有效地解決了redis分布式方面的需求
自動(dòng)將數(shù)據(jù)進(jìn)行分片,每個(gè)master上放一部分?jǐn)?shù)據(jù)
提供內(nèi)置的高可用支持,部分master不可用時(shí),還是可以繼續(xù)工作的
支撐N個(gè)redis master node,每個(gè)master node都可以?huà)燧d多個(gè)slave node
高可用,因?yàn)槊總€(gè)master都有salve節(jié)點(diǎn),那么如果mater掛掉,redis cluster這套機(jī)制,就會(huì)自動(dòng)將某個(gè)slave切換成master
redis cluster vs. replication + sentinal
如果你的數(shù)據(jù)量很少,主要是承載高并發(fā)高性能的場(chǎng)景,比如你的緩存一般就幾個(gè)G,單機(jī)足夠了
replication,一個(gè)mater,多個(gè)slave,要幾個(gè)slave跟你的要求的讀吞吐量有關(guān)系,然后自己搭建一個(gè)sentinal集群,去保證redis主從架構(gòu)的高可用性,就可以了
redis cluster,主要是針對(duì)海量數(shù)據(jù)+高并發(fā)+高可用的場(chǎng)景,海量數(shù)據(jù),如果你的數(shù)據(jù)量很大,那么建議就用redis cluster
數(shù)據(jù)分布算法
hash算法
比如你有N個(gè)redis實(shí)例,那么如何將一個(gè)key映射到redis上呢,你很可能會(huì)采用類(lèi)似下面的通用方法計(jì)算key的hash值,然后均勻的映射到到N個(gè)redis上:
hash(key)%N
如果增加一個(gè)redis,映射公式變成了hash(key)%(N+1)
如果一個(gè)redis宕機(jī)了,映射公式變成了hash(key)%(N-1)
在這兩種情況下,幾乎所有的緩存都失效了。會(huì)導(dǎo)致數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)的壓力陡增,嚴(yán)重情況,還可能導(dǎo)致數(shù)據(jù)庫(kù)宕機(jī)。
一致性hash算法
一個(gè)master宕機(jī)不會(huì)導(dǎo)致大部分緩存失效,可能存在緩存熱點(diǎn)問(wèn)題
用虛擬節(jié)點(diǎn)改進(jìn)
redis cluster的hash slot算法
redis cluster有固定的16384個(gè)hash slot,對(duì)每個(gè)key計(jì)算CRC16值,然后對(duì)16384取模,可以獲取key對(duì)應(yīng)的hash slot
redis cluster中每個(gè)master都會(huì)持有部分slot,比如有3個(gè)master,那么可能每個(gè)master持有5000多個(gè)hash slot
hash slot讓node的增加和移除很簡(jiǎn)單,增加一個(gè)master,就將其他master的hash slot移動(dòng)部分過(guò)去,減少一個(gè)master,就將它的hash slot移動(dòng)到其他master上去
移動(dòng)hash slot的成本是非常低的
客戶(hù)端的api,可以對(duì)指定的數(shù)據(jù),讓他們走同一個(gè)hash slot,通過(guò)hash tag來(lái)實(shí)現(xiàn)
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 可以將槽0-5000指派給節(jié)點(diǎn)7000負(fù)責(zé)。
每個(gè)節(jié)點(diǎn)都會(huì)記錄哪些槽指派給了自己,哪些槽指派給了其他節(jié)點(diǎn)。
客戶(hù)端向節(jié)點(diǎn)發(fā)送鍵命令,節(jié)點(diǎn)要計(jì)算這個(gè)鍵屬于哪個(gè)槽。
如果是自己負(fù)責(zé)這個(gè)槽,那么直接執(zhí)行命令,如果不是,向客戶(hù)端返回一個(gè)MOVED錯(cuò)誤,指引客戶(hù)端轉(zhuǎn)向正確的節(jié)點(diǎn)。
節(jié)點(diǎn)間的內(nèi)部通信機(jī)制
1、基礎(chǔ)通信原理
(1)redis cluster節(jié)點(diǎn)間采取gossip協(xié)議進(jìn)行通信
跟集中式不同,不是將集群元數(shù)據(jù)(節(jié)點(diǎn)信息,故障,等等)集中存儲(chǔ)在某個(gè)節(jié)點(diǎn)上,而是互相之間不斷通信,保持整個(gè)集群所有節(jié)點(diǎn)的數(shù)據(jù)是完整的
維護(hù)集群的元數(shù)據(jù)用得,集中式,一種叫做gossip
集中式:好處在于,元數(shù)據(jù)的更新和讀取,時(shí)效性非常好,一旦元數(shù)據(jù)出現(xiàn)了變更,立即就更新到集中式的存儲(chǔ)中,其他節(jié)點(diǎn)讀取的時(shí)候立即就可以感知到; 不好在于,所有的元數(shù)據(jù)的跟新壓力全部集中在一個(gè)地方,可能會(huì)導(dǎo)致元數(shù)據(jù)的存儲(chǔ)有壓力
gossip:好處在于,元數(shù)據(jù)的更新比較分散,不是集中在一個(gè)地方,更新請(qǐng)求會(huì)陸陸續(xù)續(xù),打到所有節(jié)點(diǎn)上去更新,有一定的延時(shí),降低了壓力; 缺點(diǎn),元數(shù)據(jù)更新有延時(shí),可能導(dǎo)致集群的一些操作會(huì)有一些滯后
我們剛才做reshard,去做另外一個(gè)操作,會(huì)發(fā)現(xiàn)說(shuō),configuration error,達(dá)成一致
(2)10000端口
每個(gè)節(jié)點(diǎn)都有一個(gè)專(zhuān)門(mén)用于節(jié)點(diǎn)間通信的端口,就是自己提供服務(wù)的端口號(hào)+10000,比如7001,那么用于節(jié)點(diǎn)間通信的就是17001端口
每隔節(jié)點(diǎn)每隔一段時(shí)間都會(huì)往另外幾個(gè)節(jié)點(diǎn)發(fā)送ping消息,同時(shí)其他幾點(diǎn)接收到ping之后返回pong
(3)交換的信息
故障信息,節(jié)點(diǎn)的增加和移除,hash slot信息,等等
gossip協(xié)議
gossip協(xié)議包含多種消息,包括ping,pong,meet,fail,等等
meet: 某個(gè)節(jié)點(diǎn)發(fā)送meet給新加入的節(jié)點(diǎn),讓新節(jié)點(diǎn)加入集群中,然后新節(jié)點(diǎn)就會(huì)開(kāi)始與其他節(jié)點(diǎn)進(jìn)行通信
redis-trib.rb add-node
其實(shí)內(nèi)部就是發(fā)送了一個(gè)gossip meet消息,給新加入的節(jié)點(diǎn),通知那個(gè)節(jié)點(diǎn)去加入我們的集群
ping: 每個(gè)節(jié)點(diǎn)都會(huì)頻繁給其他節(jié)點(diǎn)發(fā)送ping,其中包含自己的狀態(tài)還有自己維護(hù)的集群元數(shù)據(jù),互相通過(guò)ping交換元數(shù)據(jù)
每個(gè)節(jié)點(diǎn)每秒都會(huì)頻繁發(fā)送ping給其他的集群,ping,頻繁的互相之間交換數(shù)據(jù),互相進(jìn)行元數(shù)據(jù)的更新
pong: 返回ping和meet,包含自己的狀態(tài)和其他信息,也可以用于信息廣播和更新
fail: 某個(gè)節(jié)點(diǎn)判斷另一個(gè)節(jié)點(diǎn)fail之后,就發(fā)送fail給其他節(jié)點(diǎn),通知其他節(jié)點(diǎn),指定的節(jié)點(diǎn)宕機(jī)了
3、ping消息深入
ping很頻繁,而且要攜帶一些元數(shù)據(jù),所以可能會(huì)加重網(wǎng)絡(luò)負(fù)擔(dān)
每個(gè)節(jié)點(diǎn)每秒會(huì)執(zhí)行10次ping,每次會(huì)選擇5個(gè)最久沒(méi)有通信的其他節(jié)點(diǎn)
當(dāng)然如果發(fā)現(xiàn)某個(gè)節(jié)點(diǎn)通信延時(shí)達(dá)到了cluster_node_timeout / 2,那么立即發(fā)送ping,避免數(shù)據(jù)交換延時(shí)過(guò)長(zhǎng),落后的時(shí)間太長(zhǎng)了
比如說(shuō),兩個(gè)節(jié)點(diǎn)之間都10分鐘沒(méi)有交換數(shù)據(jù)了,那么整個(gè)集群處于嚴(yán)重的元數(shù)據(jù)不一致的情況,就會(huì)有問(wèn)題
所以cluster_node_timeout可以調(diào)節(jié),如果調(diào)節(jié)比較大,那么會(huì)降低發(fā)送的頻率
每次ping,一個(gè)是帶上自己節(jié)點(diǎn)的信息,還有就是帶上1/10其他節(jié)點(diǎn)的信息,發(fā)送出去,進(jìn)行數(shù)據(jù)交換
至少包含3個(gè)其他節(jié)點(diǎn)的信息,最多包含總節(jié)點(diǎn)-2個(gè)其他節(jié)點(diǎn)的信息
-------------------------------------------------------------------------------------------------------
面向集群的jedis內(nèi)部實(shí)現(xiàn)原理
開(kāi)發(fā),jedis,redis的java client客戶(hù)端,redis cluster,jedis cluster api
jedis cluster api與redis cluster集群交互的一些基本原理
1、基于重定向的客戶(hù)端
redis-cli -c,自動(dòng)重定向
(1)請(qǐng)求重定向
客戶(hù)端可能會(huì)挑選任意一個(gè)redis實(shí)例去發(fā)送命令,每個(gè)redis實(shí)例接收到命令,都會(huì)計(jì)算key對(duì)應(yīng)的hash slot
如果在本地就在本地處理,否則返回moved給客戶(hù)端,讓客戶(hù)端進(jìn)行重定向
cluster keyslot mykey,可以查看一個(gè)key對(duì)應(yīng)的hash slot是什么
用redis-cli的時(shí)候,可以加入-c參數(shù),支持自動(dòng)的請(qǐng)求重定向,redis-cli接收到moved之后,會(huì)自動(dòng)重定向到對(duì)應(yīng)的節(jié)點(diǎn)執(zhí)行命令
(2)計(jì)算hash slot
計(jì)算hash slot的算法,就是根據(jù)key計(jì)算CRC16值,然后對(duì)16384取模,拿到對(duì)應(yīng)的hash slot
用hash tag可以手動(dòng)指定key對(duì)應(yīng)的slot,同一個(gè)hash tag下的key,都會(huì)在一個(gè)hash slot中,比如set mykey1:{100}和set mykey2:{100}
(3)hash slot查找
節(jié)點(diǎn)間通過(guò)gossip協(xié)議進(jìn)行數(shù)據(jù)交換,就知道每個(gè)hash slot在哪個(gè)節(jié)點(diǎn)上
(4)JedisCluster的工作原理
在JedisCluster初始化的時(shí)候,就會(huì)隨機(jī)選擇一個(gè)node,初始化hashslot -> node映射表,同時(shí)為每個(gè)節(jié)點(diǎn)創(chuàng)建一個(gè)JedisPool連接池
每次基于JedisCluster執(zhí)行操作,首先JedisCluster都會(huì)在本地計(jì)算key的hashslot,然后在本地映射表找到對(duì)應(yīng)的節(jié)點(diǎn)
如果那個(gè)node正好還是持有那個(gè)hashslot,那么就ok; 如果說(shuō)進(jìn)行了reshard這樣的操作,可能hashslot已經(jīng)不在那個(gè)node上了,就會(huì)返回moved
如果JedisCluter API發(fā)現(xiàn)對(duì)應(yīng)的節(jié)點(diǎn)返回moved,那么利用該節(jié)點(diǎn)的元數(shù)據(jù),更新本地的hashslot -> node映射表緩存
重復(fù)上面幾個(gè)步驟,直到找到對(duì)應(yīng)的節(jié)點(diǎn),如果重試超過(guò)5次,那么就報(bào)錯(cuò),JedisClusterMaxRedirectionException
jedis老版本,可能會(huì)出現(xiàn)在集群某個(gè)節(jié)點(diǎn)故障還沒(méi)完成自動(dòng)切換恢復(fù)時(shí),頻繁更新hash slot,頻繁ping節(jié)點(diǎn)檢查活躍,導(dǎo)致大量網(wǎng)絡(luò)IO開(kāi)銷(xiāo)
jedis最新版本,對(duì)于這些過(guò)度的hash slot更新和ping,都進(jìn)行了優(yōu)化,避免了類(lèi)似問(wèn)題
(5)hashslot遷移和ask重定向
如果hash slot正在遷移,那么會(huì)返回ask重定向給jedis
jedis接收到ask重定向之后,會(huì)重新定位到目標(biāo)節(jié)點(diǎn)去執(zhí)行,但是因?yàn)閍sk發(fā)生在hash slot遷移過(guò)程中,所以JedisCluster API收到ask是不會(huì)更新hashslot本地緩存
已經(jīng)可以確定說(shuō),hashslot已經(jīng)遷移完了,moved是會(huì)更新本地hashslot->node映射表緩存的
-------------------------------------------------------------------------------------------------------
高可用性與主備切換原理
redis cluster的高可用的原理,幾乎跟哨兵是類(lèi)似的
1、判斷節(jié)點(diǎn)宕機(jī)
如果一個(gè)節(jié)點(diǎn)認(rèn)為另外一個(gè)節(jié)點(diǎn)宕機(jī),那么就是pfail,主觀宕機(jī)
如果多個(gè)節(jié)點(diǎn)都認(rèn)為另外一個(gè)節(jié)點(diǎn)宕機(jī)了,那么就是fail,客觀宕機(jī),跟哨兵的原理幾乎一樣,sdown,odown
在cluster-node-timeout內(nèi),某個(gè)節(jié)點(diǎn)一直沒(méi)有返回pong,那么就被認(rèn)為pfail
如果一個(gè)節(jié)點(diǎn)認(rèn)為某個(gè)節(jié)點(diǎn)pfail了,那么會(huì)在gossip ping消息中,ping給其他節(jié)點(diǎn),如果超過(guò)半數(shù)的節(jié)點(diǎn)都認(rèn)為pfail了,那么就會(huì)變成fail
2、從節(jié)點(diǎn)過(guò)濾
對(duì)宕機(jī)的master node,從其所有的slave node中,選擇一個(gè)切換成master node
檢查每個(gè)slave node與master node斷開(kāi)連接的時(shí)間,如果超過(guò)了cluster-node-timeout * cluster-slave-validity-factor,那么就沒(méi)有資格切換成master
這個(gè)也是跟哨兵是一樣的,從節(jié)點(diǎn)超時(shí)過(guò)濾的步驟
3、從節(jié)點(diǎn)選舉
哨兵:對(duì)所有從節(jié)點(diǎn)進(jìn)行排序,slave priority,offset,run id
每個(gè)從節(jié)點(diǎn),都根據(jù)自己對(duì)master復(fù)制數(shù)據(jù)的offset,來(lái)設(shè)置一個(gè)選舉時(shí)間,offset越大(復(fù)制數(shù)據(jù)越多)的從節(jié)點(diǎn),選舉時(shí)間越靠前,優(yōu)先進(jìn)行選舉
所有的master node開(kāi)始slave選舉投票,給要進(jìn)行選舉的slave進(jìn)行投票,如果大部分master node(N/2 + 1)都投票給了某個(gè)從節(jié)點(diǎn),那么選舉通過(guò),那個(gè)從節(jié)點(diǎn)可以切換成master
從節(jié)點(diǎn)執(zhí)行主備切換,從節(jié)點(diǎn)切換為主節(jié)點(diǎn)
4、與哨兵比較
整個(gè)流程跟哨兵相比,非常類(lèi)似,所以說(shuō),redis cluster功能強(qiáng)大,直接集成了replication和sentinal的功能
redis源碼篇
SDS——?jiǎng)討B(tài)字符串(原文鏈接:https://blog.csdn.net/qq193423571/java/article/details/81637075)
Redis中簡(jiǎn)單動(dòng)態(tài)字符串sds數(shù)據(jù)結(jié)構(gòu)與API相關(guān)文件是:sds.h, sds.c。
SDS本質(zhì)上就是char *,因?yàn)橛辛吮眍^sdshdr結(jié)構(gòu)的存在,所以SDS比傳統(tǒng)C字符串在某些方面更加優(yōu)秀,并且能夠兼容傳統(tǒng)C字符串。
sds在Redis中是實(shí)現(xiàn)字符串對(duì)象的工具,并且完全取代char*..sds是二進(jìn)制安全的,它可以存儲(chǔ)任意二進(jìn)制數(shù)據(jù),不像C語(yǔ)言字符串那樣以‘’來(lái)標(biāo)識(shí)字符串結(jié)束,
因?yàn)閭鹘y(tǒng)C字符串符合ASCII編碼,這種編碼的操作的特點(diǎn)就是:遇零則止 。即,當(dāng)讀一個(gè)字符串時(shí),只要遇到’’結(jié)尾,就認(rèn)為到達(dá)末尾,就忽略’’結(jié)尾以后的所有字符。因此,如果傳統(tǒng)字符串保存圖片,視頻等二進(jìn)制文件,操作文件時(shí)就被截?cái)嗔恕?/p>
SDS表頭的buf被定義為字節(jié)數(shù)組,因?yàn)榕袛嗍欠竦竭_(dá)字符串結(jié)尾的依據(jù)則是表頭的len成員,這意味著它可以存放任何二進(jìn)制的數(shù)據(jù)和文本數(shù)據(jù),包括’’
SDS 和傳統(tǒng)的 C 字符串獲得的做法不同,傳統(tǒng)的C字符串遍歷字符串的長(zhǎng)度,遇零則止,復(fù)雜度為O(n)。而SDS表頭的len成員就保存著字符串長(zhǎng)度,所以獲得字符串長(zhǎng)度的操作復(fù)雜度為O(1)。
總結(jié)下sds的特點(diǎn)是:帶著長(zhǎng)度信息的字節(jié)數(shù)組,可動(dòng)態(tài)擴(kuò)展內(nèi)存、二進(jìn)制安全、快速遍歷字符串和與傳統(tǒng)的C語(yǔ)言字符串類(lèi)型兼容。
下面是一個(gè)不同 SDS 結(jié)構(gòu)體下的不同字符串的例子:
上圖是sds的一個(gè)內(nèi)部結(jié)構(gòu)的例子。圖中展示了兩個(gè)sds字符串s1和s2的內(nèi)存結(jié)構(gòu),一個(gè)使用sdshdr8類(lèi)型的header,另一個(gè)使用sdshdr16類(lèi)型的header。但它們都表達(dá)了同樣的一個(gè)長(zhǎng)度為6的字符串的值:”tielei”。下面我們結(jié)合代碼,來(lái)解釋每一部分的組成。
sds結(jié)構(gòu)一共有五種Header定義,其目的是為了滿(mǎn)足不同長(zhǎng)度的字符串可以使用不同大小的Header,從而節(jié)省內(nèi)存。 Header部分主要包含以下幾個(gè)部分: + len:表示字符串真正的長(zhǎng)度,不包含空終止字符 + alloc:表示字符串的最大容量,不包含Header和最后的空終止字符 + flags:表示header的類(lèi)型。
2.2 在RedisObject中,SDS的兩種存儲(chǔ)形式
詳情:
> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
> debug object codehole
Value at:0x7fec2de00370 refcount:1 encoding:embstr serializedlength:45 lru:5958906 lru_seconds_idle:1
> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
> debug object codehole
Value at:0x7fec2dd0b750 refcount:1 encoding:raw serializedlength:46 lru:5958911 lru_seconds_idle:1...
一個(gè)字符的差別,存儲(chǔ)形式 encoding 就發(fā)生了變化。一個(gè)是 embstr,一個(gè)是 row。
在了解存儲(chǔ)格式的區(qū)別之前,首先了解下RedisObject結(jié)構(gòu)體。
所有的 Redis 對(duì)象都有一個(gè) Redis 對(duì)象頭結(jié)構(gòu)體
struct RedisObject {
int4 type; // 4bits 類(lèi)型
int4 encoding; // 4bits 存儲(chǔ)格式
int24 lru; // 24bits 記錄LRU信息
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj;
不同的對(duì)象具有不同的類(lèi)型 type(4bit),同一個(gè)類(lèi)型的 type 會(huì)有不同的存儲(chǔ)形式 encoding(4bit)。
為了記錄對(duì)象的 LRU 信息,使用了 24 個(gè) bit 的 lru 來(lái)記錄 LRU 信息。
每個(gè)對(duì)象都有個(gè)引用計(jì)數(shù) refcount,當(dāng)引用計(jì)數(shù)為零時(shí),對(duì)象就會(huì)被銷(xiāo)毀,內(nèi)存被回收。ptr 指針將指向?qū)ο髢?nèi)容 (body) 的具體存儲(chǔ)位置
而Redis 的字符串共有兩種存儲(chǔ)方式,在長(zhǎng)度特別短時(shí),使用 emb 形式存儲(chǔ) (embedded),當(dāng)長(zhǎng)度超過(guò) 44 時(shí),使用 raw 形式存儲(chǔ)。
embstr 存儲(chǔ)形式是這樣一種存儲(chǔ)形式,它將 RedisObject 對(duì)象頭和 SDS 對(duì)象連續(xù)存在一起,使用 malloc 方法一次分配。而 raw 存儲(chǔ)形式不一樣,它需要兩次 malloc,兩個(gè)對(duì)象頭在內(nèi)存地址上一般是不連續(xù)的。
在字符串比較小時(shí),SDS 對(duì)象頭的大小是capacity+3——SDS結(jié)構(gòu)體的內(nèi)存大小至少是 3,一個(gè) RedisObject 對(duì)象頭共需要占據(jù) 16 字節(jié)的存儲(chǔ)空間,字符串已結(jié)尾。意味著分配一個(gè)字符串的最小空間占用為 19 字節(jié) (16+3)。
如果總體超出了 64 字節(jié),Redis 認(rèn)為它是一個(gè)大字符串,不再使用 emdstr 形式存儲(chǔ),而該用 raw 形式。而64-19-結(jié)尾的,所以empstr只能容納44字節(jié)。
embstr 存儲(chǔ)形式是這樣一種存儲(chǔ)形式,它將 RedisObject 對(duì)象頭和 SDS 對(duì)象連續(xù)存在一起,使用 malloc 方法一次分配。而 raw 存儲(chǔ)形式不一樣,它需要兩次 malloc,兩個(gè)對(duì)象頭在內(nèi)存地址上一般是不連續(xù)的。
在字符串比較小時(shí),SDS 對(duì)象頭的大小是capacity+3——SDS結(jié)構(gòu)體的內(nèi)存大小至少是 3。意味著分配一個(gè)字符串的最小空間占用為 19 字節(jié) (16+3)。
如果總體超出了 64 字節(jié),Redis 認(rèn)為它是一個(gè)大字符串,不再使用 emdstr 形式存儲(chǔ),而該用 raw 形式。而64-19-結(jié)尾的,所以empstr只能容納44字節(jié)。
2.3 擴(kuò)容策略
當(dāng)字符串長(zhǎng)度小于 1M 時(shí),擴(kuò)容都是加倍現(xiàn)有的空間,如果超過(guò) 1M,擴(kuò)容時(shí)一次只會(huì)多擴(kuò) 1M 的空間
dict——字典
類(lèi)似java中的hashmap結(jié)構(gòu),但是擴(kuò)容是有所不同;
漸進(jìn)式哈希的精髓在于:數(shù)據(jù)的遷移不是一次性完成的,而是可以通過(guò)dictRehash()這個(gè)函數(shù)分步規(guī)劃的,并且調(diào)用方可以及時(shí)知道是否需要繼續(xù)進(jìn)行漸進(jìn)式哈希操作。如果dict數(shù)據(jù)結(jié)構(gòu)中存儲(chǔ)了海量的數(shù)據(jù),那么一次性遷移勢(shì)必帶來(lái)redis性能的下降,別忘了redis是單線(xiàn)程模型,在實(shí)時(shí)性要求高的場(chǎng)景下這可能是致命的。而漸進(jìn)式哈希則將這種代價(jià)可控地分?jǐn)偭耍{(diào)用方可以在dict做插入,刪除,更新的時(shí)候執(zhí)行dictRehash(),最小化數(shù)據(jù)遷移的代價(jià)。
在遷移的過(guò)程中,數(shù)據(jù)是在新表還是舊表中并不是一個(gè)非常急迫的需求,遷移的過(guò)程并不會(huì)丟失數(shù)據(jù),在舊表中找不到再到新表中尋找就是了。
dict的結(jié)構(gòu)大致如上,接下來(lái)分析一下其中最重要的幾個(gè)數(shù)據(jù)成員:
dictht::table:哈希表內(nèi)部的table結(jié)構(gòu)使用了鏈地址法來(lái)解決哈希沖突,剛開(kāi)始看的時(shí)候我很奇怪,這怎么是個(gè)二維數(shù)組?這其實(shí)是一個(gè)指向數(shù)組的指針,數(shù)組中的每一項(xiàng)都是entry鏈表的頭結(jié)點(diǎn)。
dictht ht[2]:在dict的內(nèi)部,維護(hù)了兩張哈希表,作用等同于是一對(duì)滾動(dòng)數(shù)組,一張表是舊表,一張表是新表,當(dāng)hashtable的大小需要?jiǎng)討B(tài)改變的時(shí)候,舊表中的元素就往新開(kāi)辟的新表中遷移,當(dāng)下一次變動(dòng)大小,當(dāng)前的新表又變成了舊表,以此達(dá)到資源的復(fù)用和效率的提升。
rehashidx:因?yàn)槭菨u進(jìn)式的哈希,數(shù)據(jù)的遷移并不是一步完成的,所以需要有一個(gè)索引來(lái)指示當(dāng)前的rehash進(jìn)度。當(dāng)rehashidx為-1時(shí),代表沒(méi)有哈希操作。
————————————————
rehash是以bucket(桶)為基本單位進(jìn)行漸進(jìn)式的數(shù)據(jù)遷移的,每步完成一個(gè)bucket的遷移,直至所有數(shù)據(jù)遷移完畢。一個(gè)bucket對(duì)應(yīng)哈希表數(shù)組中的一條entry鏈表。新版本的dictRehash()還加入了一個(gè)最大訪(fǎng)問(wèn)空桶數(shù)(empty_visits)的限制來(lái)進(jìn)一步減小可能引起阻塞的時(shí)間。
————————————————
最后是從《Redis設(shè)計(jì)與實(shí)現(xiàn)》中copy來(lái)的圖解,可以幫助大家更形象地理解整個(gè)incremental rehash的過(guò)程:
set——實(shí)現(xiàn)方式也是字典,只不過(guò)所有的value都是null,其他特性和字典一摸一樣
zipList壓縮列表
簡(jiǎn)介
壓縮列表是 Redis 為了節(jié)約內(nèi)存而開(kāi)發(fā)的, 由一系列特殊編碼的連續(xù)內(nèi)存塊組成的,
增加元素
因?yàn)閦iplist緊湊,意味著每插入一個(gè)元素都要調(diào)用realloc擴(kuò)展內(nèi)存,依據(jù)待擴(kuò)展內(nèi)存的大小決定是,一次性拷貝新地址還是,在原地址擴(kuò)展,如果ziplist內(nèi)存過(guò)大重新分配內(nèi)存可拷貝內(nèi)存代價(jià)過(guò)高,故不適合存儲(chǔ)大型字符串字符串
級(jí)聯(lián)更新
因?yàn)閦iplist每個(gè)元素entry都會(huì)有一個(gè)prevlen存儲(chǔ)前一個(gè)entry的長(zhǎng)度,如果內(nèi)容小于254,prevlen=1bit 否則prevlen=5bit。這意味著如果某個(gè)entry經(jīng)過(guò)修改從253到254直接,那么他的下一個(gè)entry的prevlen字段就要更新,從1->5,如果后面這個(gè)entry的長(zhǎng)度也是253,便產(chǎn)生二樓級(jí)聯(lián)更新
inset小整數(shù)集合
簡(jiǎn)介
集合元素都是整數(shù)并且元素個(gè)數(shù)較少時(shí)使用,當(dāng)set里面放入非整數(shù)是,存儲(chǔ)形式立即從inset轉(zhuǎn)變成hash
quicklist快速列表
概述
考慮到鏈表的附加空間相對(duì)太高,prev 和 next 指針就要占去 16 個(gè)字節(jié) (64bit 系統(tǒng)的指針是 8 個(gè)字節(jié)),另外每個(gè)節(jié)點(diǎn)的內(nèi)存都是單獨(dú)分配,會(huì)加劇內(nèi)存的碎片化,影響內(nèi)存管理效率。
后續(xù)版本對(duì)列表數(shù)據(jù)結(jié)構(gòu)進(jìn)行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
基本結(jié)構(gòu)
quickList 是 zipList 和 linkedList 的混合體,它將 linkedList 按段切分,每一段使用 zipList 來(lái)緊湊存儲(chǔ),多個(gè) zipList 之間使用雙向指針串接起來(lái)。
壓縮深度
quicklist 默認(rèn)的壓縮深度是 0,也就是不壓縮。壓縮的實(shí)際深度由配置參數(shù)lis搞t-compress-depth決定。
為了支持快速的 push/pop 操作,quicklist 的首尾兩個(gè) ziplist 不壓縮,此時(shí)深度就是 1。
如果深度為 2,就表示 quicklist 的首尾第一個(gè) ziplist 以及首尾第二個(gè) ziplist 都不壓縮。
zipList 長(zhǎng)度
quicklist 內(nèi)部默認(rèn)單個(gè) ziplist 長(zhǎng)度為 8k 字節(jié),超出了這個(gè)字節(jié)數(shù),就會(huì)新起一個(gè) ziplist。
ziplist 的長(zhǎng)度由配置參數(shù) list-max-ziplist-size 決定。
skiplist數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)介
skiplist本質(zhì)上也是一種查找結(jié)構(gòu),用于解決算法中的查找問(wèn)題(Searching),即根據(jù)給定的key,快速查到它所在的位置(或者對(duì)應(yīng)的value)。
我們?cè)凇禦edis內(nèi)部數(shù)據(jù)結(jié)構(gòu)詳解》系列的第一篇中介紹dict的時(shí)候,曾經(jīng)討論過(guò):一般查找問(wèn)題的解法分為兩個(gè)大類(lèi):一個(gè)是基于各種平衡樹(shù),一個(gè)是基于哈希表。但skiplist卻比較特殊,它沒(méi)法歸屬到這兩大類(lèi)里面。
這種數(shù)據(jù)結(jié)構(gòu)是由William Pugh發(fā)明的,最早出現(xiàn)于他在1990年發(fā)表的論文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。對(duì)細(xì)節(jié)感興趣的同學(xué)可以下載論文原文來(lái)閱讀。
skiplist,顧名思義,首先它是一個(gè)list。實(shí)際上,它是在有序鏈表的基礎(chǔ)上發(fā)展起來(lái)的。
我們先來(lái)看一個(gè)有序鏈表,如下圖(最左側(cè)的灰色節(jié)點(diǎn)表示一個(gè)空的頭結(jié)點(diǎn)):
在這樣一個(gè)鏈表中,如果我們要查找某個(gè)數(shù)據(jù),那么需要從頭開(kāi)始逐個(gè)進(jìn)行比較,直到找到包含數(shù)據(jù)的那個(gè)節(jié)點(diǎn),或者找到第一個(gè)比給定數(shù)據(jù)大的節(jié)點(diǎn)為止(沒(méi)找到)。也就是說(shuō),時(shí)間復(fù)雜度為O(n)。同樣,當(dāng)我們要插入新數(shù)據(jù)的時(shí)候,也要經(jīng)歷同樣的查找過(guò)程,從而確定插入位置。
假如我們每相鄰兩個(gè)節(jié)點(diǎn)增加一個(gè)指針,讓指針指向下下個(gè)節(jié)點(diǎn),如下圖:
這樣所有新增加的指針連成了一個(gè)新的鏈表,但它包含的節(jié)點(diǎn)個(gè)數(shù)只有原來(lái)的一半(上圖中是7, 19, 26)。現(xiàn)在當(dāng)我們想查找數(shù)據(jù)的時(shí)候,可以先沿著這個(gè)新鏈表進(jìn)行查找。當(dāng)碰到比待查數(shù)據(jù)大的節(jié)點(diǎn)時(shí),再回到原來(lái)的鏈表中進(jìn)行查找。比如,我們想查找23,查找的路徑是沿著下圖中標(biāo)紅的指針?biāo)赶虻姆较蜻M(jìn)行的:
23首先和7比較,再和19比較,比它們都大,繼續(xù)向后比較。
但23和26比較的時(shí)候,比26要小,因此回到下面的鏈表(原鏈表),與22比較。
23比22要大,沿下面的指針繼續(xù)向后和26比較。23比26小,說(shuō)明待查數(shù)據(jù)23在原鏈表中不存在,而且它的插入位置應(yīng)該在22和26之間。
在這個(gè)查找過(guò)程中,由于新增加的指針,我們不再需要與鏈表中每個(gè)節(jié)點(diǎn)逐個(gè)進(jìn)行比較了。需要比較的節(jié)點(diǎn)數(shù)大概只有原來(lái)的一半。
利用同樣的方式,我們可以在上層新產(chǎn)生的鏈表上,繼續(xù)為每相鄰的兩個(gè)節(jié)點(diǎn)增加一個(gè)指針,從而產(chǎn)生第三層鏈表。如下圖:
在這個(gè)新的三層鏈表結(jié)構(gòu)上,如果我們還是查找23,那么沿著最上層鏈表首先要比較的是19,發(fā)現(xiàn)23比19大,接下來(lái)我們就知道只需要到19的后面去繼續(xù)查找,從而一下子跳過(guò)了19前面的所有節(jié)點(diǎn)。可以想象,當(dāng)鏈表足夠長(zhǎng)的時(shí)候,這種多層鏈表的查找方式能讓我們跳過(guò)很多下層節(jié)點(diǎn),大大加快查找的速度。
skiplist正是受這種多層鏈表的想法的啟發(fā)而設(shè)計(jì)出來(lái)的。實(shí)際上,按照上面生成鏈表的方式,上面每一層鏈表的節(jié)點(diǎn)個(gè)數(shù),是下面一層的節(jié)點(diǎn)個(gè)數(shù)的一半,這樣查找過(guò)程就非常類(lèi)似于一個(gè)二分查找,使得查找的時(shí)間復(fù)雜度可以降低到O(log n)。但是,這種方法在插入數(shù)據(jù)的時(shí)候有很大的問(wèn)題。新插入一個(gè)節(jié)點(diǎn)之后,就會(huì)打亂上下相鄰兩層鏈表上節(jié)點(diǎn)個(gè)數(shù)嚴(yán)格的2:1的對(duì)應(yīng)關(guān)系。如果要維持這種對(duì)應(yīng)關(guān)系,就必須把新插入的節(jié)點(diǎn)后面的所有節(jié)點(diǎn)(也包括新插入的節(jié)點(diǎn))重新進(jìn)行調(diào)整,這會(huì)讓時(shí)間復(fù)雜度重新蛻化成O(n)。刪除數(shù)據(jù)也有同樣的問(wèn)題。
skiplist為了避免這一問(wèn)題,它不要求上下相鄰兩層鏈表之間的節(jié)點(diǎn)個(gè)數(shù)有嚴(yán)格的對(duì)應(yīng)關(guān)系,而是為每個(gè)節(jié)點(diǎn)隨機(jī)出一個(gè)層數(shù)(level)。比如,一個(gè)節(jié)點(diǎn)隨機(jī)出的層數(shù)是3,那么就把它鏈入到第1層到第3層這三層鏈表中。為了表達(dá)清楚,下圖展示了如何通過(guò)一步步的插入操作從而形成一個(gè)skiplist的過(guò)程:
從上面skiplist的創(chuàng)建和插入過(guò)程可以看出,每一個(gè)節(jié)點(diǎn)的層數(shù)(level)是隨機(jī)出來(lái)的,而且新插入一個(gè)節(jié)點(diǎn)不會(huì)影響其它節(jié)點(diǎn)的層數(shù)。因此,插入操作只需要修改插入節(jié)點(diǎn)前后的指針,而不需要對(duì)很多節(jié)點(diǎn)都進(jìn)行調(diào)整。這就降低了插入操作的復(fù)雜度。實(shí)際上,這是skiplist的一個(gè)很重要的特性,這讓它在插入性能上明顯優(yōu)于平衡樹(shù)的方案。這在后面我們還會(huì)提到。
根據(jù)上圖中的skiplist結(jié)構(gòu),我們很容易理解這種數(shù)據(jù)結(jié)構(gòu)的名字的由來(lái)。skiplist,翻譯成中文,可以翻譯成“跳表”或“跳躍表”,指的就是除了最下面第1層鏈表之外,它會(huì)產(chǎn)生若干層稀疏的鏈表,這些鏈表里面的指針故意跳過(guò)了一些節(jié)點(diǎn)(而且越高層的鏈表跳過(guò)的節(jié)點(diǎn)越多)。這就使得我們?cè)诓檎覕?shù)據(jù)的時(shí)候能夠先在高層的鏈表中進(jìn)行查找,然后逐層降低,最終降到第1層鏈表來(lái)精確地確定數(shù)據(jù)位置。在這個(gè)過(guò)程中,我們跳過(guò)了一些節(jié)點(diǎn),從而也就加快了查找速度。
剛剛創(chuàng)建的這個(gè)skiplist總共包含4層鏈表,現(xiàn)在假設(shè)我們?cè)谒锩嬉廊徊檎?3,下圖給出了查找路徑:
總結(jié)
以上是生活随笔為你收集整理的深入理解redis原理!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python画图小猪佩奇_吊炸天!Pyt
- 下一篇: Twitter 又遭投诉,被指“非法”解