分布式緩存技術(shù)PK:選擇Redis還是Memcached?
經(jīng)平臺(tái)同意授權(quán)轉(zhuǎn)載
作者:田京昆(騰訊后臺(tái)研發(fā)工程師)
來(lái)源:騰云閣(https://www.qcloud.com/community/article/129)
Memcached和Redis,作為近些年最常用的緩存服務(wù)器,相信大家對(duì)它們?cè)偈煜げ贿^(guò)了裂问。為了對(duì)它們有更深入的了解,我曾經(jīng)讀過(guò)它們的主要源碼牛柒,下面我將從個(gè)人角度簡(jiǎn)單對(duì)比一下它們的實(shí)現(xiàn)方式堪簿,有理解錯(cuò)誤之處,歡迎指正皮壁。
文中使用的架構(gòu)類(lèi)的圖片大多來(lái)自于網(wǎng)絡(luò)椭更,有部分圖與最新實(shí)現(xiàn)有出入,文中已經(jīng)指出蛾魄。
一虑瀑、綜述
讀一個(gè)軟件的源碼湿滓,首先要弄懂軟件是用作干什么的,那Memcached和Redis是干啥的舌狗?眾所周知叽奥,數(shù)據(jù)一般會(huì)放在數(shù)據(jù)庫(kù)中,但是查詢數(shù)據(jù)會(huì)相對(duì)比較慢痛侍,特別是用戶很多時(shí)朝氓,頻繁的查詢,需要耗費(fèi)大量的時(shí)間主届。怎么辦呢膀篮?數(shù)據(jù)放在哪里查詢快?那肯定是內(nèi)存中岂膳。
Memcached和Redis就是將數(shù)據(jù)存儲(chǔ)在內(nèi)存中,按照key-value的方式查詢磅网,可以大幅度提高效率谈截。所以一般它們都用做緩存服務(wù)器,緩存常用的數(shù)據(jù)涧偷,需要查詢的時(shí)候簸喂,直接從它們那兒獲取,減少查詢數(shù)據(jù)庫(kù)的次數(shù)燎潮,提高查詢效率喻鳄。
二、服務(wù)方式
Memcached和Redis怎么提供服務(wù)呢确封?它們是獨(dú)立的進(jìn)程除呵,需要的話,還可以讓他們變成daemon進(jìn)程爪喘,所以我們的用戶進(jìn)程要使用Memcached和Redis的服務(wù)的話颜曾,就需要進(jìn)程間通信了”#考慮到用戶進(jìn)程和Memcached和Redis不一定在同一臺(tái)機(jī)器上泛豪,所以還需要支持網(wǎng)絡(luò)間通信。
因此侦鹏,Memcached和Redis自己本身就是網(wǎng)絡(luò)服務(wù)器诡曙,用戶進(jìn)程通過(guò)與他們通過(guò)網(wǎng)絡(luò)來(lái)傳輸數(shù)據(jù),顯然最簡(jiǎn)單和最常用的就是使用tcp連接了略水。另外价卤,Memcached和Redis都支持udp協(xié)議。而且當(dāng)用戶進(jìn)程和Memcached和Redis在同一機(jī)器時(shí)聚请,還可以使用unix域套接字通信荠雕。
三稳其、事件模型
下面開(kāi)始講他們具體是怎么實(shí)現(xiàn)的了。首先來(lái)看一下它們的事件模型炸卑。
自從epoll出來(lái)以后既鞠,幾乎所有的網(wǎng)絡(luò)服務(wù)器全都拋棄select和poll,換成了epoll。Redis也一樣持痰,只不多它還提供對(duì)select和poll的支持叫潦,可以自己配置使用哪一個(gè),但是一般都是用epoll洒敏。另外針對(duì)BSD,還支持使用kqueue疙驾。而Memcached是基于libevent的凶伙,不過(guò)libevent底層也是使用epoll的,所以可以認(rèn)為它們都是使用epoll它碎。epoll的特性這里就不介紹了函荣,網(wǎng)上介紹文章很多。
它們都使用epoll來(lái)做事件循環(huán)扳肛,不過(guò)Redis是單線程的服務(wù)器(Redis也是多線程的傻挂,只不過(guò)除了主線程以外,其他線程沒(méi)有event loop挖息,只是會(huì)進(jìn)行一些后臺(tái)存儲(chǔ)工作)金拒,而Memcached是多線程的。 Redis的事件模型很簡(jiǎn)單套腹,只有一個(gè)event loop绪抛,是簡(jiǎn)單的reactor實(shí)現(xiàn)。不過(guò)Redis事件模型中有一個(gè)亮點(diǎn)沉迹,我們知道epoll是針對(duì)fd的睦疫,它返回的就緒事件也是只有fd,Redis里面的fd就是服務(wù)器與客戶端連接的socket的fd鞭呕,但是處理的時(shí)候蛤育,需要根據(jù)這個(gè)fd找到具體的客戶端的信息,怎么找呢葫松?通常的處理方式就是用紅黑樹(shù)將fd與客戶端信息保存起來(lái)瓦糕,通過(guò)fd查找,效率是lgn腋么。
不過(guò)Redis比較特殊咕娄,Redis的客戶端的數(shù)量上限可以設(shè)置,即可以知道同一時(shí)刻珊擂,Redis所打開(kāi)的fd的上限圣勒,而我們知道费变,進(jìn)程的fd在同一時(shí)刻是不會(huì)重復(fù)的(fd只有關(guān)閉后才能復(fù)用),所以Redis使用一個(gè)數(shù)組圣贸,將fd作為數(shù)組的下標(biāo)挚歧,數(shù)組的元素就是客戶端的信息,這樣吁峻,直接通過(guò)fd就能定位客戶端信息滑负,查找效率是O(1),還省去了復(fù)雜的紅黑樹(shù)的實(shí)現(xiàn)(我曾經(jīng)用c寫(xiě)一個(gè)網(wǎng)絡(luò)服務(wù)器用含,就因?yàn)橐3謋d和connect對(duì)應(yīng)關(guān)系矮慕,不想自己寫(xiě)紅黑樹(shù),然后用了STL里面的set啄骇,導(dǎo)致項(xiàng)目變成了c++的痴鳄,最后項(xiàng)目使用g++編譯,這事我不說(shuō)誰(shuí)知道缸夹?)夏跷。顯然這種方式只能針對(duì)connection數(shù)量上限已確定,并且不是太大的網(wǎng)絡(luò)服務(wù)器明未,像nginx這種http服務(wù)器就不適用,nginx就是自己寫(xiě)了紅黑樹(shù)壹蔓。
而Memcached是多線程的趟妥,使用master-worker的方式,主線程監(jiān)聽(tīng)端口佣蓉,建立連接披摄,然后順序分配給各個(gè)工作線程。每一個(gè)從線程都有一個(gè)event loop勇凭,它們服務(wù)不同的客戶端疚膊。master線程和worker線程之間使用管道通信,每一個(gè)工作線程都會(huì)創(chuàng)建一個(gè)管道虾标,然后保存寫(xiě)端和讀端寓盗,并且將讀端加入event loop,監(jiān)聽(tīng)可讀事件璧函。
同時(shí)傀蚌,每個(gè)從線程都有一個(gè)就緒連接隊(duì)列,主線程連接連接后蘸吓,將連接的item放入這個(gè)隊(duì)列善炫,然后往該線程的管道的寫(xiě)端寫(xiě)入一個(gè)connect命令,這樣event loop中加入的管道讀端就會(huì)就緒库继,從線程讀取命令箩艺,解析命令發(fā)現(xiàn)是有連接窜醉,然后就會(huì)去自己的就緒隊(duì)列中獲取連接,并進(jìn)行處理艺谆。多線程的優(yōu)勢(shì)就是可以充分發(fā)揮多核的優(yōu)勢(shì)榨惰,不過(guò)編寫(xiě)程序麻煩一點(diǎn),Memcached里面就有各種鎖和條件變量來(lái)進(jìn)行線程同步擂涛。
四读串、內(nèi)存分配
Memcached和Redis的核心任務(wù)都是在內(nèi)存中操作數(shù)據(jù),內(nèi)存管理自然是核心的內(nèi)容撒妈。
首先看看他們的內(nèi)存分配方式恢暖。Memcached有自己的內(nèi)存池,即預(yù)先分配一大塊內(nèi)存狰右,然后接下來(lái)分配內(nèi)存就從內(nèi)存池中分配杰捂,這樣可以減少內(nèi)存分配的次數(shù),提高效率棋蚌,這也是大部分網(wǎng)絡(luò)服務(wù)器的實(shí)現(xiàn)方式嫁佳,只不過(guò)各個(gè)內(nèi)存池的管理方式根據(jù)具體情況而不同。而Redis沒(méi)有自己得內(nèi)存池谷暮,而是直接使用時(shí)分配蒿往,即什么時(shí)候需要什么時(shí)候分配,內(nèi)存管理的事交給內(nèi)核湿弦,自己只負(fù)責(zé)取和釋放(Redis既是單線程瓤漏,又沒(méi)有自己的內(nèi)存池,是不是感覺(jué)實(shí)現(xiàn)的太簡(jiǎn)單了颊埃?那是因?yàn)樗闹攸c(diǎn)都放在數(shù)據(jù)庫(kù)模塊了)蔬充。不過(guò)Redis支持使用tcmalloc來(lái)替換glibc的malloc,前者是Google的產(chǎn)品班利,比glibc的malloc快饥漫。
由于Redis沒(méi)有自己的內(nèi)存池,所以內(nèi)存申請(qǐng)和釋放的管理就簡(jiǎn)單很多罗标,直接malloc和free即可庸队,十分方便。而Memcached是支持內(nèi)存池的闯割,所以內(nèi)存申請(qǐng)是從內(nèi)存池中獲取皿哨,而free也是還給內(nèi)存池,所以需要很多額外的管理操作纽谒,實(shí)現(xiàn)起來(lái)麻煩很多证膨,具體的會(huì)在后面Memcached的slab機(jī)制講解中分析。
五鼓黔、數(shù)據(jù)庫(kù)實(shí)現(xiàn)
接下來(lái)看看他們的最核心內(nèi)容央勒,各自數(shù)據(jù)庫(kù)的實(shí)現(xiàn)不见。
1、
Memcached數(shù)據(jù)庫(kù)實(shí)現(xiàn)
Memcached只支持key-value崔步,即只能一個(gè)key對(duì)于一個(gè)value稳吮。它的數(shù)據(jù)在內(nèi)存中也是這樣以key-value對(duì)的方式存儲(chǔ),它使用slab機(jī)制井濒。
首先看Memcached是如何存儲(chǔ)數(shù)據(jù)的灶似,即存儲(chǔ)key-value對(duì)。如下圖瑞你,每一個(gè)key-value對(duì)都存儲(chǔ)在一個(gè)item結(jié)構(gòu)中酪惭,包含了相關(guān)的屬性和key和value的值。
item是保存key-value對(duì)的者甲,當(dāng)item多的時(shí)候春感,怎么查找特定的item是個(gè)問(wèn)題。
所以memcached維護(hù)了一個(gè)hash表虏缸,它用于快速查找item鲫懒。hash表適用開(kāi)鏈法(與redis一樣)解決鍵的沖突,每一個(gè)hash表的桶里面存儲(chǔ)了一個(gè)鏈表刽辙,鏈表節(jié)點(diǎn)就是item的指針窥岩,如上圖中的h_next就是指桶里面的鏈表的下一個(gè)節(jié)點(diǎn)。 hash表支持?jǐn)U容(item的數(shù)量是桶的數(shù)量的1.5以上時(shí)擴(kuò)容)宰缤,有一個(gè)primary_hashtable谦秧,還有一個(gè)old_hashtable,其中正常適用primary_hashtable撵溃,但是擴(kuò)容的時(shí)候,將old_hashtable = primary_hashtable锥累,然后primary_hashtable設(shè)置為新申請(qǐng)的hash表(桶的數(shù)量乘以2)缘挑,然后依次將old_hashtable 里面的數(shù)據(jù)往新的hash表里面移動(dòng),并用一個(gè)變量expand_bucket記錄以及移動(dòng)了多少個(gè)桶桶略,移動(dòng)完成后语淘,再free原來(lái)的old_hashtable 即可(Redis也是有兩個(gè)hash表,也是移動(dòng)际歼,不過(guò)不是后臺(tái)線程完成惶翻,而是每次移動(dòng)一個(gè)桶)。擴(kuò)容的操作鹅心,專(zhuān)門(mén)有一個(gè)后臺(tái)擴(kuò)容的線程來(lái)完成吕粗,需要擴(kuò)容的時(shí)候,使用條件變量通知它旭愧,完成擴(kuò)容后颅筋,它又考試阻塞等待擴(kuò)容的條件變量宙暇。
這樣在擴(kuò)容的時(shí)候,查找一個(gè)item可能會(huì)在primary_hashtable和old_hashtable的任意一個(gè)中议泵,需要根據(jù)比較它的桶的位置和expand_bucket的大小來(lái)比較確定它在哪個(gè)表里占贫。
item是從哪里分配的呢?從slab中先口。如下圖型奥,Memcached有很多slabclass,它們管理slab碉京,每一個(gè)slab其實(shí)是trunk的集合厢汹,真正的item是在trunk中分配的,一個(gè)trunk分配一個(gè)item收夸。一個(gè)slab中的trunk的大小一樣坑匠,不同的slab,trunk的大小按比例遞增卧惜,需要新申請(qǐng)一個(gè)item的時(shí)候厘灼,根據(jù)它的大小來(lái)選擇trunk,規(guī)則是比它大的最小的那個(gè)trunk咽瓷。
這樣设凹,不同大小的item就分配在不同的slab中,歸不同的slabclass管理茅姜。 這樣的缺點(diǎn)是會(huì)有部分內(nèi)存浪費(fèi)闪朱,因?yàn)橐粋€(gè)trunk可能比item大,如圖2钻洒,分配100B的item的時(shí)候奋姿,選擇112的trunk,但是會(huì)有12B的浪費(fèi)素标,這部分內(nèi)存資源沒(méi)有使用称诗。
如上圖,整個(gè)構(gòu)造就是這樣头遭,slabclass管理slab寓免,一個(gè)slabclass有一個(gè)slab_list,可以管理多個(gè)slab计维,同一個(gè)slabclass中的slab的trunk大小都一樣袜香。slabclass有一個(gè)指針slot,保存了未分配的item已經(jīng)被free掉的item(不是真的free內(nèi)存鲫惶,只是不用了而已)蜈首,有item不用的時(shí)候,就放入slot的頭部,這樣每次需要在當(dāng)前slab中分配item的時(shí)候疾就,直接取slot取即可澜术,不用管item是未分配過(guò)的還是被釋放掉的。
然后猬腰,每一個(gè)slabclass對(duì)應(yīng)一個(gè)鏈表鸟废,有head數(shù)組和tail數(shù)組,它們分別保存了鏈表的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)姑荷。鏈表中的節(jié)點(diǎn)就是改slabclass所分配的item盒延,新分配的放在頭部,鏈表越往后的item鼠冕,表示它已經(jīng)很久沒(méi)有被使用了添寺。當(dāng)slabclass的內(nèi)存不足,需要?jiǎng)h除一些過(guò)期item的時(shí)候懈费,就可以從鏈表的尾部開(kāi)始刪除计露,沒(méi)錯(cuò),這個(gè)鏈表就是為了實(shí)現(xiàn)LRU憎乙。光靠它還不行票罐,因?yàn)殒湵淼牟樵兪荗(n)的,所以定位item的時(shí)候泞边,使用hash表该押,這已經(jīng)有了,所有分配的item已經(jīng)在hash表中了阵谚,因此掌猛,hash用于查找item橱赠,然后鏈表有用存儲(chǔ)item的最近使用順序,這也是lru的標(biāo)準(zhǔn)實(shí)現(xiàn)方法哎媚。
每次需要新分配item的時(shí)候灌闺,找到slabclass對(duì)于的鏈表虐沥,從尾部往前找码党,看item是否已經(jīng)過(guò)期疟呐,過(guò)期的話,直接就用這個(gè)過(guò)期的item當(dāng)做新的item翼馆。沒(méi)有過(guò)期的,則需要從slab中分配trunk金度,如果slab用完了应媚,則需要往slabclass中添加slab了。
Memcached支持設(shè)置過(guò)期時(shí)間猜极,即expire time中姜,但是內(nèi)部并不定期檢查數(shù)據(jù)是否過(guò)期,而是客戶進(jìn)程使用該數(shù)據(jù)的時(shí)候,Memcached會(huì)檢查expire time丢胚,如果過(guò)期翩瓜,直接返回錯(cuò)誤。這樣的優(yōu)點(diǎn)是携龟,不需要額外的cpu來(lái)進(jìn)行expire time的檢查兔跌,缺點(diǎn)是有可能過(guò)期數(shù)據(jù)很久不被使用,則一直沒(méi)有被釋放峡蟋,占用內(nèi)存坟桅。
Memcached是多線程的,而且只維護(hù)了一個(gè)數(shù)據(jù)庫(kù)蕊蝗,所以可能有多個(gè)客戶進(jìn)程操作同一個(gè)數(shù)據(jù)仅乓,這就有可能產(chǎn)生問(wèn)題。比如蓬戚,A已經(jīng)把數(shù)據(jù)更改了夸楣,然后B也更改了改數(shù)據(jù),那么A的操作就被覆蓋了子漩,而可能A不知道豫喧,A任務(wù)數(shù)據(jù)現(xiàn)在的狀態(tài)時(shí)他改完后的那個(gè)值,這樣就可能產(chǎn)生問(wèn)題痛单。為了解決這個(gè)問(wèn)題嘿棘,Memcached使用了CAS協(xié)議,簡(jiǎn)單說(shuō)就是item保存一個(gè)64位的unsigned int值旭绒,標(biāo)記數(shù)據(jù)的版本鸟妙,每更新一次(數(shù)據(jù)值有修改),版本號(hào)增加挥吵,然后每次對(duì)數(shù)據(jù)進(jìn)行更改操作重父,需要比對(duì)客戶進(jìn)程傳來(lái)的版本號(hào)和服務(wù)器這邊item的版本號(hào)是否一致,一致則可進(jìn)行更改操作忽匈,否則提示臟數(shù)據(jù)房午。
以上就是Memcached如何實(shí)現(xiàn)一個(gè)key-value的數(shù)據(jù)庫(kù)的介紹。
2丹允、
Redis數(shù)據(jù)庫(kù)實(shí)現(xiàn)
首先Redis數(shù)據(jù)庫(kù)的功能強(qiáng)大一些郭厌,因?yàn)椴幌馦emcached只支持保存字符串,Redis支持string雕蔽、 list折柠、 set、sorted set批狐、hash table 5種數(shù)據(jù)結(jié)構(gòu)扇售。例如存儲(chǔ)一個(gè)人的信息就可以使用hash table,用人的名字做key,然后name super承冰, age 24华弓, 通過(guò)key 和 name,就可以取到名字super困乒,或者通過(guò)key和age寂屏,就可以取到年齡24。這樣顶燕,當(dāng)只需要取得age的時(shí)候凑保,不需要把人的整個(gè)信息取回來(lái),然后從里面找age涌攻,直接獲取age即可欧引,高效方便。
為了實(shí)現(xiàn)這些數(shù)據(jù)結(jié)構(gòu)恳谎,Redis定義了抽象的對(duì)象Redis object芝此,如下圖。每一個(gè)對(duì)象有類(lèi)型因痛,一共5種:字符串婚苹,鏈表,集合鸵膏,有序集合膊升,哈希表。同時(shí)谭企,為了提高效率廓译,Redis為每種類(lèi)型準(zhǔn)備了多種實(shí)現(xiàn)方式,根據(jù)特定的場(chǎng)景來(lái)選擇合適的實(shí)現(xiàn)方式债查,encoding就是表示對(duì)象的實(shí)現(xiàn)方式的非区。還有記錄了對(duì)象的lru,即上次被訪問(wèn)的時(shí)間盹廷,同時(shí)在Redis 服務(wù)器中會(huì)記錄一個(gè)當(dāng)前的時(shí)間(近似值征绸,因?yàn)檫@個(gè)時(shí)間只是每隔一定時(shí)間,服務(wù)器進(jìn)行自動(dòng)維護(hù)的時(shí)候才更新)俄占,它們兩個(gè)只差就可以計(jì)算出對(duì)象多久沒(méi)有被訪問(wèn)了管怠。
然后Redis object中還有引用計(jì)數(shù),這是為了共享對(duì)象缸榄,然后確定對(duì)象的刪除時(shí)間用的渤弛。最后使用一個(gè)void*指針來(lái)指向?qū)ο蟮恼嬲齼?nèi)容。正式由于使用了抽象Redis object碰凶,使得數(shù)據(jù)庫(kù)操作數(shù)據(jù)時(shí)方便很多暮芭,全部統(tǒng)一使用Redis object對(duì)象即可,需要區(qū)分對(duì)象類(lèi)型的時(shí)候欲低,再根據(jù)type來(lái)判斷辕宏。而且正式由于采用了這種面向?qū)ο蟮姆椒ǎ宺edis的代碼看起來(lái)很像c++代碼砾莱,其實(shí)全是用c寫(xiě)的瑞筐。
說(shuō)到底R(shí)edis還是一個(gè)key-value的數(shù)據(jù)庫(kù),不管它支持多少種數(shù)據(jù)結(jié)構(gòu)腊瑟,最終存儲(chǔ)的還是以key-value的方式聚假,只不過(guò)value可以是鏈表,set闰非,sorted set膘格,hash table等。和Memcached一樣财松,所有的key都是string瘪贱,而set,sorted set辆毡,hash table等具體存儲(chǔ)的時(shí)候也用到了string菜秦。 而c沒(méi)有現(xiàn)成的string,所以redis的首要任務(wù)就是實(shí)現(xiàn)一個(gè)string舶掖,取名叫sds(simple dynamic string)球昨,如下的代碼, 非常簡(jiǎn)單的一個(gè)結(jié)構(gòu)體眨攘,len存儲(chǔ)改string的內(nèi)存總長(zhǎng)度主慰,free表示還有多少字節(jié)沒(méi)有使用,而buf存儲(chǔ)具體的數(shù)據(jù)期犬,顯然len-free就是目前字符串的長(zhǎng)度河哑。
字符串解決了,所有的key都存成sds就行了龟虎,那么key和value怎么關(guān)聯(lián)呢璃谨?key-value的格式在腳本語(yǔ)言中很好處理,直接使用字典即可鲤妥,C沒(méi)有字典佳吞,怎么辦呢?自己寫(xiě)一個(gè)唄(Redis十分熱衷于造輪子)棉安〉装猓看下面的代碼,privdata存額外信息贡耽,用的很少衷模,至少我們發(fā)現(xiàn)鹊汛。 dictht是具體的哈希表,一個(gè)dict對(duì)應(yīng)兩張哈希表阱冶,這是為了擴(kuò)容(包括rehashidx也是為了擴(kuò)容)刁憋。dictType存儲(chǔ)了哈希表的屬性。redis還為dict實(shí)現(xiàn)了迭代器(所以說(shuō)看起來(lái)像c++代碼)木蹬。
哈希表的具體實(shí)現(xiàn)是和mc類(lèi)似的做法至耻,也是使用開(kāi)鏈法來(lái)解決沖突,不過(guò)里面用到了一些小技巧镊叁。比如使用dictType存儲(chǔ)函數(shù)指針尘颓,可以動(dòng)態(tài)配置桶里面元素的操作方法。又比如dictht中保存的sizemask取size(桶的數(shù)量)-1晦譬,用它與key做&操作來(lái)代替取余運(yùn)算疤苹,加快速度等等×搽纾總的來(lái)看痰催,dict里面有兩個(gè)哈希表,每個(gè)哈希表的桶里面存儲(chǔ)dictEntry鏈表迎瞧,dictEntry存儲(chǔ)具體的key和value夸溶。
前面說(shuō)過(guò),一個(gè)dict對(duì)于兩個(gè)dictht凶硅,是為了擴(kuò)容(其實(shí)還有縮容)缝裁。正常的時(shí)候,dict只使用dictht[0]足绅,當(dāng)dict[0]中已有entry的數(shù)量與桶的數(shù)量達(dá)到一定的比例后捷绑,就會(huì)觸發(fā)擴(kuò)容和縮容操作,我們統(tǒng)稱(chēng)為rehash氢妈,這時(shí)粹污,為dictht[1]申請(qǐng)rehash后的大小的內(nèi)存,然后把dictht[0]里的數(shù)據(jù)往dictht[1]里面移動(dòng)首量,并用rehashidx記錄當(dāng)前已經(jīng)移動(dòng)萬(wàn)的桶的數(shù)量壮吩,當(dāng)所有桶都移完后,rehash完成加缘,這時(shí)將dictht[1]變成dictht[0], 將原來(lái)的dictht[0]變成dictht[1]鸭叙,并變?yōu)閚ull即可。
不同于Memcached拣宏,這里不用開(kāi)一個(gè)后臺(tái)線程來(lái)做沈贝,而是就在event loop中完成,并且rehash不是一次性完成勋乾,而是分成多次宋下,每次用戶操作dict之前嗡善,Redis移動(dòng)一個(gè)桶的數(shù)據(jù),直到rehash完成学歧。這樣就把移動(dòng)分成多個(gè)小移動(dòng)完成滤奈,把rehash的時(shí)間開(kāi)銷(xiāo)均分到用戶每個(gè)操作上,這樣避免了用戶一個(gè)請(qǐng)求導(dǎo)致rehash的時(shí)候撩满,需要等待很長(zhǎng)時(shí)間,直到rehash完成才有返回的情況绅你。不過(guò)在rehash期間伺帘,每個(gè)操作都變慢了點(diǎn),而且用戶還不知道redis在他的請(qǐng)求中間添加了移動(dòng)數(shù)據(jù)的操作忌锯,感覺(jué)Redis太賤了 :-D
有了dict伪嫁,數(shù)據(jù)庫(kù)就好實(shí)現(xiàn)了。所有數(shù)據(jù)讀存儲(chǔ)在dict中偶垮,key存儲(chǔ)成dictEntry中的key(string)张咳,用void* 指向一個(gè)redis object,它可以是5種類(lèi)型中的任何一種似舵。如下圖脚猾,結(jié)構(gòu)構(gòu)造是這樣,不過(guò)這個(gè)圖已經(jīng)過(guò)時(shí)了砚哗,有一些與Redis 3.0不符合的地方龙助。
5種type的對(duì)象,每一個(gè)都至少有兩種底層實(shí)現(xiàn)方式蛛芥。string有3種:
REDIS_ENCODING_RAW提鸟;
REDIS_ENCIDING_INT;
REDIS_ENCODING_EMBSTR仅淑;
list有:普通雙向鏈表和壓縮鏈表称勋,壓縮鏈表簡(jiǎn)單的說(shuō),就是講數(shù)組改造成鏈表涯竟,連續(xù)的空間赡鲜,然后通過(guò)存儲(chǔ)字符串的大小信息來(lái)模擬鏈表,相對(duì)普通鏈表來(lái)說(shuō)可以節(jié)省空間庐船,不過(guò)有副作用蝗蛙,由于是連續(xù)的空間,所以改變內(nèi)存大小的時(shí)候醉鳖,需要重新分配捡硅,并且由于保存了字符串的字節(jié)大小,所有有可能引起連續(xù)更新(具體實(shí)現(xiàn)請(qǐng)?jiān)敿?xì)看代碼)盗棵。
set有dict和intset(全是整數(shù)的時(shí)候使用它來(lái)存儲(chǔ))壮韭, sorted set有:skiplist和ziplist北发, hashtable實(shí)現(xiàn)有壓縮列表和dict和ziplist。skiplist就是跳表喷屋,它有接近于紅黑樹(shù)的效率琳拨,但是實(shí)現(xiàn)起來(lái)比紅黑樹(shù)簡(jiǎn)單很多,所以被采用(奇怪屯曹,這里又不造輪子了狱庇,難道因?yàn)檫@個(gè)輪子有點(diǎn)難?)恶耽。 hash table可以使用dict實(shí)現(xiàn)密任,則改dict中,每個(gè)dictentry中key保存了key(這是哈希表中的鍵值對(duì)的key)偷俭,而value則保存了value浪讳,它們都是string。 而set中的dict涌萤,每個(gè)dictentry中key保存了set中具體的一個(gè)元素的值淹遵,value則為null爷光。
圖中的zset(有序集合)有誤畅姊,zset使用skiplist和ziplist實(shí)現(xiàn),skiplist很好理解幻锁,就把它當(dāng)做紅黑樹(shù)的替代品就行川抡,和紅黑樹(shù)一樣淌实,它也可以排序。怎么用ziplist存儲(chǔ)zset呢猖腕?首先在zset中拆祈,每個(gè)set中的元素都有一個(gè)分值score,用它來(lái)排序倘感。所以在ziplist中放坏,按照分值大小,先存元素老玛,再存它的score淤年,再存下一個(gè)元素,然后score蜡豹。這樣連續(xù)存儲(chǔ)麸粮,所以插入或者刪除的時(shí)候,都需要重新分配內(nèi)存镜廉。所以當(dāng)元素超過(guò)一定數(shù)量弄诲,或者某個(gè)元素的字符數(shù)超過(guò)一定數(shù)量,Redis就會(huì)選擇使用skiplist來(lái)實(shí)現(xiàn)zset(如果當(dāng)前使用的是ziplist,會(huì)將這個(gè)ziplist中的數(shù)據(jù)取出齐遵,存入一個(gè)新的skiplist寂玲,然后刪除改ziplist,這就是底層實(shí)現(xiàn)轉(zhuǎn)換梗摇,其余類(lèi)型的redis object也是可以轉(zhuǎn)換的)拓哟。
另外,ziplist如何實(shí)現(xiàn)hashtable呢伶授?其實(shí)也很簡(jiǎn)單断序,就是存儲(chǔ)一個(gè)key,存儲(chǔ)一個(gè)value糜烹,再存儲(chǔ)一個(gè)key违诗,再存儲(chǔ)一個(gè)value。還是順序存儲(chǔ)景图,與zset實(shí)現(xiàn)類(lèi)似,所以當(dāng)元素超過(guò)一定數(shù)量碉哑,或者某個(gè)元素的字符數(shù)超過(guò)一定數(shù)量時(shí)挚币,就會(huì)轉(zhuǎn)換成hashtable來(lái)實(shí)現(xiàn)。各種底層實(shí)現(xiàn)方式是可以轉(zhuǎn)換的扣典,Redis可以根據(jù)情況選擇最合適的實(shí)現(xiàn)方式妆毕,這也是這樣使用類(lèi)似面向?qū)ο蟮膶?shí)現(xiàn)方式的好處。
需要指出的是贮尖,使用skiplist來(lái)實(shí)現(xiàn)zset的時(shí)候笛粘,其實(shí)還用了一個(gè)dict,這個(gè)dict存儲(chǔ)一樣的鍵值對(duì)湿硝。為什么呢薪前?因?yàn)閟kiplist的查找只是lgn的(可能變成n),而dict可以到O(1)关斜, 所以使用一個(gè)dict來(lái)加速查找示括,由于skiplist和dict可以指向同一個(gè)redis object,所以不會(huì)浪費(fèi)太多內(nèi)存痢畜。另外使用ziplist實(shí)現(xiàn)zset的時(shí)候垛膝,為什么不用dict來(lái)加速查找呢?因?yàn)閦iplist支持的元素個(gè)數(shù)很少(個(gè)數(shù)多時(shí)就轉(zhuǎn)換成skiplist了)丁稀,順序遍歷也很快吼拥,所以不用dict了。
這樣看來(lái)线衫,上面的dict凿可,dictType,dictHt授账,dictEntry矿酵,redis object都是很有考量的唬复,它們配合實(shí)現(xiàn)了一個(gè)具有面向?qū)ο笊实撵`活、高效數(shù)據(jù)庫(kù)全肮。不得不說(shuō)敞咧,redis數(shù)據(jù)庫(kù)的設(shè)計(jì)還是很厲害的。
與Memcached不同的是辜腺,Redis的數(shù)據(jù)庫(kù)不止一個(gè)休建,默認(rèn)就有16個(gè),編號(hào)0-15评疗〔馍埃客戶可以選擇使用哪一個(gè)數(shù)據(jù)庫(kù),默認(rèn)使用0號(hào)數(shù)據(jù)庫(kù)百匆。 不同的數(shù)據(jù)庫(kù)數(shù)據(jù)不共享砌些,即在不同的數(shù)據(jù)庫(kù)中可以存在同樣的key,但是在同一個(gè)數(shù)據(jù)庫(kù)中加匈,key必須是唯一的存璃。
Redis也支持expire time的設(shè)置,我們看上面的redis object雕拼,里面沒(méi)有保存expire的字段纵东,那redis怎么記錄數(shù)據(jù)的expire time呢? Redis是為每個(gè)數(shù)據(jù)庫(kù)又增加了一個(gè)dict啥寇,這個(gè)dict叫expire dict偎球,它里面的dict entry里面的key就是數(shù)對(duì)的key,而value全是數(shù)據(jù)為64位int的redis object辑甜,這個(gè)int就是expire time衰絮。這樣,判斷一個(gè)key是否過(guò)期的時(shí)候磷醋,去expire dict里面找到它岂傲,取出expire time比對(duì)當(dāng)前時(shí)間即可。為什么這樣做呢子檀? 因?yàn)椴⒉皇撬械膋ey都會(huì)設(shè)置過(guò)期時(shí)間镊掖,所以,對(duì)于不設(shè)置expire time的key來(lái)說(shuō)褂痰,保存一個(gè)expire time會(huì)浪費(fèi)空間亩进,而是用expire dict來(lái)單獨(dú)保存的話,可以根據(jù)需要靈活使用內(nèi)存(檢測(cè)到key過(guò)期時(shí)缩歪,會(huì)把它從expire dict中刪除)归薛。
Redis的expire 機(jī)制是怎樣的呢? 與Memcahed類(lèi)似,Redis也是惰性刪除主籍,即要用到數(shù)據(jù)時(shí)习贫,先檢查key是否過(guò)期,過(guò)期則刪除千元,然后返回錯(cuò)誤苫昌。單純的靠惰性刪除,上面說(shuō)過(guò)可能會(huì)導(dǎo)致內(nèi)存浪費(fèi)幸海,所以Redis也有補(bǔ)充方案祟身,Redis里面有個(gè)定時(shí)執(zhí)行的函數(shù),叫servercron物独,它是維護(hù)服務(wù)器的函數(shù)袜硫,在它里面,會(huì)對(duì)過(guò)期數(shù)據(jù)進(jìn)行刪除挡篓,注意不是全刪婉陷,而是在一定的時(shí)間內(nèi),對(duì)每個(gè)數(shù)據(jù)庫(kù)的expire dict里面的數(shù)據(jù)隨機(jī)選取出來(lái)官研,如果過(guò)期秽澳,則刪除,否則再選阀参,直到規(guī)定的時(shí)間到肝集。即隨機(jī)選取過(guò)期的數(shù)據(jù)刪除瞻坝,這個(gè)操作的時(shí)間分兩種蛛壳,一種較長(zhǎng),一種較短所刀,一般執(zhí)行短時(shí)間的刪除衙荐,每隔一定的時(shí)間,執(zhí)行一次長(zhǎng)時(shí)間的刪除浮创。這樣可以有效的緩解光采用惰性刪除而導(dǎo)致的內(nèi)存浪費(fèi)問(wèn)題忧吟。
4、
Redis數(shù)據(jù)庫(kù)持久化
Redis和memcached的最大不同斩披,就是Redis支持?jǐn)?shù)據(jù)持久化溜族,這也是很多人選擇使用Redis而不是Memcached的最大原因。Redis的持久化垦沉,分為兩種策略煌抒,用戶可以配置使用不同的策略。
RDB持久化
用戶執(zhí)行save或者bgsave的時(shí)候厕倍,就會(huì)觸發(fā)RDB持久化操作寡壮。RDB持久化操作的核心思想就是把數(shù)據(jù)庫(kù)原封不動(dòng)的保存在文件里。
那如何存儲(chǔ)呢?如下圖况既, 首先存儲(chǔ)一個(gè)Redis字符串这溅,起到驗(yàn)證的作用,表示是RDB文件棒仍,然后保存Redis的版本信息悲靴,然后是具體的數(shù)據(jù)庫(kù),然后存儲(chǔ)結(jié)束符EOF降狠,最后用檢驗(yàn)和对竣。關(guān)鍵就是databases,看它的名字也知道榜配,它存儲(chǔ)了多個(gè)數(shù)據(jù)庫(kù)否纬,數(shù)據(jù)庫(kù)按照編號(hào)順序存儲(chǔ),0號(hào)數(shù)據(jù)庫(kù)存儲(chǔ)完了蛋褥,才輪到1临燃,然后是2, 一直到最后一個(gè)數(shù)據(jù)庫(kù)。
每一個(gè)數(shù)據(jù)庫(kù)存儲(chǔ)方式如下烙心,首先一個(gè)1字節(jié)的常量SELECTDB膜廊,表示切換DB了,然后下一個(gè)接上數(shù)據(jù)庫(kù)的編號(hào)淫茵,它的長(zhǎng)度是可變的爪瓜,然后接下來(lái)就是具體的key-value對(duì)的數(shù)據(jù)了。
由上面的代碼也可以看出匙瘪,存儲(chǔ)的時(shí)候铆铆,先檢查expire time,如果已經(jīng)過(guò)期丹喻,不存就行了薄货,否則,則將expire time存下來(lái)碍论,注意谅猾,及時(shí)是存儲(chǔ)expire time,也是先存儲(chǔ)它的類(lèi)型為REDIS_RDB_OPCODE_EXPIRETIME_MS鳍悠,然后再存儲(chǔ)具體過(guò)期時(shí)間税娜。接下來(lái)存儲(chǔ)真正的key-value對(duì),首先存儲(chǔ)value的類(lèi)型藏研,然后存儲(chǔ)key(它按照字符串存儲(chǔ))敬矩,然后存儲(chǔ)value,如下圖遥倦。
在rdbsaveobject中谤绳,會(huì)根據(jù)val的不同類(lèi)型占锯,按照不同的方式存儲(chǔ),不過(guò)從根本上來(lái)看缩筛,最終都是轉(zhuǎn)換成字符串存儲(chǔ)消略,比如val是一個(gè)linklist,那么先存儲(chǔ)整個(gè)list的字節(jié)數(shù)瞎抛,然后遍歷這個(gè)list艺演,把數(shù)據(jù)取出來(lái),依次按照string寫(xiě)入文件桐臊。對(duì)于hash table胎撤,也是先計(jì)算字節(jié)數(shù),然后依次取出hash table中的dictEntry断凶,按照string的方式存儲(chǔ)它的key和value伤提,然后存儲(chǔ)下一個(gè)dictEntry。
總之认烁,RDB的存儲(chǔ)方式肿男,對(duì)一個(gè)key-value對(duì),會(huì)先存儲(chǔ)expire time(如果有的話)却嗡,然后是value的類(lèi)型舶沛,然后存儲(chǔ)key(字符串方式),然后根據(jù)value的類(lèi)型和底層實(shí)現(xiàn)方式窗价,將value轉(zhuǎn)換成字符串存儲(chǔ)如庭。這里面為了實(shí)現(xiàn)數(shù)據(jù)壓縮,以及能夠根據(jù)文件恢復(fù)數(shù)據(jù)撼港,redis使用了很多編碼的技巧坪它,有些我也沒(méi)太看懂,不過(guò)關(guān)鍵還是要理解思想餐胀,不要在意這些細(xì)節(jié)哟楷。
保存了RDB文件瘤载,當(dāng)Redis再啟動(dòng)的時(shí)候否灾,就根據(jù)RDB文件來(lái)恢復(fù)數(shù)據(jù)庫(kù)。由于以及在RDB文件中保存了數(shù)據(jù)庫(kù)的號(hào)碼鸣奔,以及它包含的key-value對(duì)墨技,以及每個(gè)key-value對(duì)中value的具體類(lèi)型,實(shí)現(xiàn)方式挎狸,和數(shù)據(jù)扣汪,redis只要順序讀取文件,然后恢復(fù)object即可锨匆。由于保存了expire time崭别,發(fā)現(xiàn)當(dāng)前的時(shí)間已經(jīng)比expire time大了冬筒,即數(shù)據(jù)已經(jīng)超時(shí)了,則不恢復(fù)這個(gè)key-value對(duì)即可茅主。
保存RDB文件是一個(gè)很巨大的工程舞痰,所以Redis還提供后臺(tái)保存的機(jī)制。即執(zhí)行bgsave的時(shí)候诀姚,Redis fork出一個(gè)子進(jìn)程响牛,讓子進(jìn)程來(lái)執(zhí)行保存的工作,而父進(jìn)程繼續(xù)提供redis正常的數(shù)據(jù)庫(kù)服務(wù)赫段。由于子進(jìn)程復(fù)制了父進(jìn)程的地址空間呀打,即子進(jìn)程擁有父進(jìn)程fork時(shí)的數(shù)據(jù)庫(kù),子進(jìn)程執(zhí)行save的操作糯笙,把它從父進(jìn)程那兒繼承來(lái)的數(shù)據(jù)庫(kù)寫(xiě)入一個(gè)temp文件即可贬丛。在子進(jìn)程復(fù)制期間,Redis會(huì)記錄數(shù)據(jù)庫(kù)的修改次數(shù)(dirty)给涕。當(dāng)子進(jìn)程完成時(shí)瘫寝,發(fā)送給父進(jìn)程SIGUSR1信號(hào),父進(jìn)程捕捉到這個(gè)信號(hào)稠炬,就知道子進(jìn)程完成了復(fù)制焕阿,然后父進(jìn)程將子進(jìn)程保存的temp文件改名為真正的RDB文件(即真正保存成功了才改成目標(biāo)文件,這才是保險(xiǎn)的做法)首启。然后記錄下這一次save的結(jié)束時(shí)間暮屡。
這里有一個(gè)問(wèn)題,在子進(jìn)程保存期間毅桃,父進(jìn)程的數(shù)據(jù)庫(kù)已經(jīng)被修改了褒纲,而父進(jìn)程只是記錄了修改的次數(shù)(dirty),被沒(méi)有進(jìn)行修正操作钥飞。似乎使得RDB保存的不是實(shí)時(shí)的數(shù)據(jù)庫(kù)莺掠,有點(diǎn)不太高大上的樣子。 不過(guò)后面要介紹的AOF持久化读宙,就解決了這個(gè)問(wèn)題彻秆。
除了客戶執(zhí)行sava或者bgsave命令署惯,還可以配置RDB保存條件恩掷。即在配置文件中配置坟瓢,在t時(shí)間內(nèi)仓技,數(shù)據(jù)庫(kù)被修改了dirty次猜煮,則進(jìn)行后臺(tái)保存辛润。Redis在serve cron的時(shí)候第煮,會(huì)根據(jù)dirty數(shù)目和上次保存的時(shí)間撩炊,來(lái)判斷是否符合條件结耀,符合條件的話留夜,就進(jìn)行bg save匙铡,注意,任意時(shí)刻只能有一個(gè)子進(jìn)程來(lái)進(jìn)行后臺(tái)保存碍粥,因?yàn)楸4媸莻€(gè)很費(fèi)io的操作慰枕,多個(gè)進(jìn)程大量io效率不行,而且不好管理即纲。
AOF持久化
首先想一個(gè)問(wèn)題具帮,保存數(shù)據(jù)庫(kù)一定需要像RDB那樣把數(shù)據(jù)庫(kù)里面的所有數(shù)據(jù)保存下來(lái)么?有沒(méi)有別的方法低斋?
RDB保存的只是最終的數(shù)據(jù)庫(kù)蜂厅,它是一個(gè)結(jié)果。結(jié)果是怎么來(lái)的膊畴?是通過(guò)用戶的各個(gè)命令建立起來(lái)的掘猿,所以可以不保存結(jié)果,而只保存建立這個(gè)結(jié)果的命令唇跨。Redis的AOF就是這個(gè)思想稠通,它不同RDB保存db的數(shù)據(jù),它保存的是一條一條建立數(shù)據(jù)庫(kù)的命令买猖。
我們首先來(lái)看AOF文件的格式改橘,它里面保存的是一條一條的命令,首先存儲(chǔ)命令長(zhǎng)度玉控,然后存儲(chǔ)命令飞主,具體的分隔符什么的可以自己深入研究,這都不是重點(diǎn)高诺,反正知道AOF文件存儲(chǔ)的是Redis客戶端執(zhí)行的命令即可碌识。
Redis server中有一個(gè)sds aof_buf, 如果aof持久化打開(kāi)的話,每個(gè)修改數(shù)據(jù)庫(kù)的命令都會(huì)存入這個(gè)aof_buf(保存的是aof文件中命令格式的字符串)虱而,然后event loop沒(méi)循環(huán)一次筏餐,在server cron中調(diào)用flushaofbuf,把a(bǔ)of_buf中的命令寫(xiě)入aof文件(其實(shí)是write牡拇,真正寫(xiě)入的是內(nèi)核緩沖區(qū))魁瞪,再清空aof_buf,進(jìn)入下一次loop诅迷。這樣所有的數(shù)據(jù)庫(kù)的變化佩番,都可以通過(guò)aof文件中的命令來(lái)還原众旗,達(dá)到了保存數(shù)據(jù)庫(kù)的效果罢杉。
需要注意的是,flushaofbuf中調(diào)用的write贡歧,它只是把數(shù)據(jù)寫(xiě)入了內(nèi)核緩沖區(qū)滩租,真正寫(xiě)入文件時(shí)內(nèi)核自己決定的赋秀,可能需要延后一段時(shí)間。 不過(guò)Redis支持配置律想,可以配置每次寫(xiě)入后sync猎莲,則在Redis里面調(diào)用sync,將內(nèi)核中的數(shù)據(jù)寫(xiě)入文件技即,這不過(guò)這要耗費(fèi)一次系統(tǒng)調(diào)用著洼,耗費(fèi)時(shí)間而已。還可以配置策略為1秒鐘sync一次而叼,則Redis會(huì)開(kāi)啟一個(gè)后臺(tái)線程(所以說(shuō)Redis不是單線程身笤,只是單eventloop而已),這個(gè)后臺(tái)線程會(huì)每一秒調(diào)用一次sync葵陵。這里要問(wèn)了液荸,RDB的時(shí)候?yàn)槭裁礇](méi)有考慮sync的事情呢?因?yàn)镽DB是一次性存儲(chǔ)的脱篙,不像AOF這樣多次存儲(chǔ)娇钱,RDB的時(shí)候調(diào)用一次sync也沒(méi)什么影響,而且使用bg save的時(shí)候绊困,子進(jìn)程會(huì)自己退出(Exit)文搂,這時(shí)候Exit函數(shù)內(nèi)會(huì)沖刷緩沖區(qū),自動(dòng)就寫(xiě)入了文件中秤朗。
再來(lái)看细疚,如果不想使用aof_buf保存每次的修改命令,也可以使用aof持久化川梅。
Redis提供aof_rewrite疯兼,即根據(jù)現(xiàn)有的數(shù)據(jù)庫(kù)生成命令,然后把命令寫(xiě)入aof文件中贫途。很奇特吧吧彪?對(duì),就是這么厲害丢早。進(jìn)行aof_rewrite的時(shí)候姨裸,Redis變量每個(gè)數(shù)據(jù)庫(kù),然后根據(jù)key-value對(duì)中value的具體類(lèi)型怨酝,生成不同的命令傀缩,比如是list,則它生成一個(gè)保存list的命令农猬,這個(gè)命令里包含了保存該list所需要的的數(shù)據(jù)赡艰,如果這個(gè)list數(shù)據(jù)過(guò)長(zhǎng),還會(huì)分成多條命令斤葱,先創(chuàng)建這個(gè)list慷垮,然后往list里面添加元素揖闸,總之,就是根據(jù)數(shù)據(jù)反向生成保存數(shù)據(jù)的命令料身。然后將這些命令存儲(chǔ)aof文件汤纸,這樣不就和aof append達(dá)到同樣的效果了么?
再來(lái)看芹血,aof格式也支持后臺(tái)模式贮泞。執(zhí)行aof_bgrewrite的時(shí)候,也是fork一個(gè)子進(jìn)程幔烛,然后讓子進(jìn)程進(jìn)行aof_rewrite隙畜,把它復(fù)制的數(shù)據(jù)庫(kù)寫(xiě)入一個(gè)臨時(shí)文件,然后寫(xiě)完后用新號(hào)通知父進(jìn)程说贝。父進(jìn)程判斷子進(jìn)程的退出信息是否正確议惰,然后將臨時(shí)文件更名成最終的aof文件。好了乡恕,問(wèn)題來(lái)了言询。在子進(jìn)程持久化期間,可能父進(jìn)程的數(shù)據(jù)庫(kù)有更新傲宜,怎么把這個(gè)更新通知子進(jìn)程呢运杭?難道要用進(jìn)程間通信么?是不是有點(diǎn)麻煩呢函卒?你猜redis怎么做的辆憔?它根本不通知子進(jìn)程。什么报嵌,不通知虱咧?那更新怎么辦? 在子進(jìn)程執(zhí)行aof_bgrewrite期間锚国,父進(jìn)程會(huì)保存所有對(duì)數(shù)據(jù)庫(kù)有更改的操作的命令(增腕巡,刪除,改等)血筑,把他們保存在aof_rewrite_buf_blocks中绘沉,這是一個(gè)鏈表,每個(gè)block都可以保存命令豺总,存不下時(shí)车伞,新申請(qǐng)block,然后放入鏈表后面即可喻喳,當(dāng)子進(jìn)程通知完成保存后另玖,父進(jìn)程將aof_rewrite_buf_blocks的命令append 進(jìn)aof文件就可以了。多么優(yōu)美的設(shè)計(jì),想一想自己當(dāng)初還考慮用進(jìn)程間通信日矫,別人直接用最簡(jiǎn)單的方法就完美的解決了問(wèn)題赂弓,有句話說(shuō)得真對(duì)绑榴,越優(yōu)秀的設(shè)計(jì)越趨于簡(jiǎn)單哪轿,而復(fù)雜的東西往往都是靠不住的。
至于aof文件的載入翔怎,也就是一條一條的執(zhí)行aof文件里面的命令而已窃诉。不過(guò)考慮到這些命令就是客戶端發(fā)送給Redis的命令,所以Redis干脆生成了一個(gè)假的客戶端赤套,它沒(méi)有和Redis建立網(wǎng)絡(luò)連接飘痛,而是直接執(zhí)行命令即可。首先搞清楚容握,這里的假的客戶端宣脉,并不是真正的客戶端,而是存儲(chǔ)在Redis里面的客戶端的信息剔氏,里面有寫(xiě)和讀的緩沖區(qū)塑猖,它是存在于Redis服務(wù)器中的。所以谈跛,如下圖羊苟,直接讀入aof的命令,放入客戶端的讀緩沖區(qū)中感憾,然后執(zhí)行這個(gè)客戶端的命令即可蜡励。這樣就完成了aof文件的載入。
整個(gè)aof持久化的設(shè)計(jì)阻桅,個(gè)人認(rèn)為相當(dāng)精彩凉倚。其中有很多地方,值得膜拜嫂沉。
5占遥、
Redis的事務(wù)
Redis另一個(gè)比Memcached強(qiáng)大的地方,是它支持簡(jiǎn)單的事務(wù)输瓜。事務(wù)簡(jiǎn)單說(shuō)就是把幾個(gè)命令合并瓦胎,一次性執(zhí)行全部命令。對(duì)于關(guān)系型數(shù)據(jù)庫(kù)來(lái)說(shuō)尤揣,事務(wù)還有回滾機(jī)制搔啊,即事務(wù)命令要么全部執(zhí)行成功,只要有一條失敗就回滾北戏,回到事務(wù)執(zhí)行前的狀態(tài)负芋。Redis不支持回滾,它的事務(wù)只保證命令依次被執(zhí)行,即使中間一條命令出錯(cuò)也會(huì)繼續(xù)往下執(zhí)行旧蛾,所以說(shuō)它只支持簡(jiǎn)單的事務(wù)莽龟。
首先看Redis事務(wù)的執(zhí)行過(guò)程。首先執(zhí)行multi命令锨天,表示開(kāi)始事務(wù)毯盈,然后輸入需要執(zhí)行的命令,最后輸入exec執(zhí)行事務(wù)病袄。Redis服務(wù)器收到multi命令后搂赋,會(huì)將對(duì)應(yīng)的client的狀態(tài)設(shè)置為REDIS_MULTI,表示client處于事務(wù)階段益缠,并在client的multiState結(jié)構(gòu)體里面保持事務(wù)的命令具體信息(當(dāng)然首先也會(huì)檢查命令是否能否識(shí)別脑奠,錯(cuò)誤的命令不會(huì)保存),即命令的個(gè)數(shù)和具體的各個(gè)命令幅慌,當(dāng)收到exec命令后宋欺,Redis會(huì)順序執(zhí)行multiState里面保存的命令,然后保存每個(gè)命令的返回值胰伍,當(dāng)有命令發(fā)生錯(cuò)誤的時(shí)候齿诞,Redis不會(huì)停止事務(wù),而是保存錯(cuò)誤信息喇辽,然后繼續(xù)往下執(zhí)行掌挚,當(dāng)所有的命令都執(zhí)行完后,將所有命令的返回值一起返回給客戶菩咨。
Redis為什么不支持回滾呢吠式?網(wǎng)上看到的解釋出現(xiàn)問(wèn)題是由于客戶程序的問(wèn)題,所以沒(méi)必要服務(wù)器回滾抽米,同時(shí)特占,不支持回滾,Redis服務(wù)器的運(yùn)行高效很多云茸。在我看來(lái)是目,Redis的事務(wù)不是傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)的事務(wù),要求CIAD那么非常嚴(yán)格标捺,或者說(shuō)Redis的事務(wù)都不是事務(wù)懊纳,只是提供了一種方式,使得客戶端可以一次性執(zhí)行多條命令而已亡容,就把事務(wù)當(dāng)做普通命令就行了嗤疯,支持回滾也就沒(méi)必要了。
我們知道Redis是單event loop的闺兢,在真正執(zhí)行一個(gè)事物的時(shí)候(即redis收到exec命令后)茂缚,事物的執(zhí)行過(guò)程是不會(huì)被打斷的,所有命令都會(huì)在一個(gè)event loop中執(zhí)行完。但是在用戶逐個(gè)輸入事務(wù)的命令的時(shí)候脚囊,這期間龟糕,可能已經(jīng)有別的客戶修改了事務(wù)里面用到的數(shù)據(jù),這就可能產(chǎn)生問(wèn)題悔耘。所以Redis還提供了watch命令讲岁,用戶可以在輸入multi之前,執(zhí)行watch命令淮逊,指定需要觀察的數(shù)據(jù)催首,這樣如果在exec之前扶踊,有其他的客戶端修改了這些被watch的數(shù)據(jù)泄鹏,則exec的時(shí)候,執(zhí)行到處理被修改的數(shù)據(jù)的命令的時(shí)候秧耗,會(huì)執(zhí)行失敗备籽,提示數(shù)據(jù)已經(jīng)dirty。 這是如何是實(shí)現(xiàn)的呢分井? 原來(lái)在每一個(gè)RedisDb中還有一個(gè)dict watched_keys车猬,watched_kesy中dictentry的key是被watch的數(shù)據(jù)庫(kù)的key,而value則是一個(gè)list尺锚,里面存儲(chǔ)的是watch它的client珠闰。
同時(shí),每個(gè)client也有一個(gè)watched_keys瘫辩,里面保存的是這個(gè)client當(dāng)前watch的key伏嗜。在執(zhí)行watch的時(shí)候,Redis在對(duì)應(yīng)的數(shù)據(jù)庫(kù)的watched_keys中找到這個(gè)key(如果沒(méi)有伐厌,則新建一個(gè)dictentry)承绸,然后在它的客戶列表中加入這個(gè)client,同時(shí)挣轨,往這個(gè)client的watched_keys中加入這個(gè)key军熏。當(dāng)有客戶執(zhí)行一個(gè)命令修改數(shù)據(jù)的時(shí)候,Redis首先在watched_keys中找這個(gè)key卷扮,如果發(fā)現(xiàn)有它荡澎,證明有client在watch它,則遍歷所有watch它的client晤锹,將這些client設(shè)置為REDIS_DIRTY_CAS摩幔,表面有watch的key被dirty了。當(dāng)客戶執(zhí)行的事務(wù)的時(shí)候抖甘,首先會(huì)檢查是否被設(shè)置了REDIS_DIRTY_CAS热鞍,如果是,則表明數(shù)據(jù)dirty了,事務(wù)無(wú)法執(zhí)行薇宠,會(huì)立即返回錯(cuò)誤偷办,只有client沒(méi)有被設(shè)置REDIS_DIRTY_CAS的時(shí)候才能夠執(zhí)行事務(wù)。 需要指出的是澄港,執(zhí)行exec后椒涯,該client的所有watch的key都會(huì)被清除,同時(shí)DB中該key的client列表也會(huì)清除該client回梧,即執(zhí)行exec后废岂,該client不再watch任何key(即使exec沒(méi)有執(zhí)行成功也是一樣)。所以說(shuō)redis的事務(wù)是簡(jiǎn)單的事務(wù)狱意,算不上真正的事務(wù)湖苞。
以上就是Redis的事務(wù),感覺(jué)實(shí)現(xiàn)很簡(jiǎn)單详囤,實(shí)際用處也不是太大财骨。
6、
Redis的發(fā)布訂閱頻道
Redis支持頻道藏姐,即加入一個(gè)頻道的用戶相當(dāng)于加入了一個(gè)群隆箩,客戶往頻道里面發(fā)的信息,頻道里的所有client都能收到羔杨。
實(shí)現(xiàn)也很簡(jiǎn)單捌臊,也watch_keys實(shí)現(xiàn)差不多,Redis server中保存了一個(gè)pubsub_channels的dict兜材,里面的key是頻道的名稱(chēng)(顯然要唯一了)理澎,value則是一個(gè)鏈表,保存加入了該頻道的client护姆。同時(shí)矾端,每個(gè)client都有一個(gè)pubsub_channels,保存了自己關(guān)注的頻道卵皂。當(dāng)用用戶往頻道發(fā)消息的時(shí)候秩铆,首先在server中的pubsub_channels找到改頻道,然后遍歷client灯变,給他們發(fā)消息殴玛。而訂閱,取消訂閱頻道不夠都是操作pubsub_channels而已添祸,很好理解滚粟。
同時(shí),redis還支持模式頻道刃泌。即通過(guò)正則匹配頻道凡壤,如有模式頻道p,?1, 則向普通頻道p1發(fā)送消息時(shí)署尤,會(huì)匹配p,1亚侠,除了往普通頻道發(fā)消息外曹体,還會(huì)往p,1模式頻道中的client發(fā)消息硝烂。
注意箕别,這里是用發(fā)布命令里面的普通頻道來(lái)匹配已有的模式頻道,而不是在發(fā)布命令里制定模式頻道滞谢,然后匹配Redis里面保存的頻道串稀。實(shí)現(xiàn)方式也很簡(jiǎn)單,在Redis server里面有個(gè)pubsub_patterns的list(這里為什么不用dict狮杨?因?yàn)閜ubsub_patterns的個(gè)數(shù)一般較少母截,不需要使用dict,簡(jiǎn)單的list就好了)禾酱,它里面存儲(chǔ)的是pubsubPattern結(jié)構(gòu)體微酬,里面是模式和client信息绘趋,如下所示颤陶,一個(gè)模式,一個(gè)client陷遮,所以如果有多個(gè)clint監(jiān)聽(tīng)一個(gè)pubsub_patterns的話滓走,在list面會(huì)有多個(gè)pubsubPattern,保存client和pubsub_patterns的對(duì)應(yīng)關(guān)系帽馋。 同時(shí)搅方,在client里面,也有一個(gè)pubsub_patterns list绽族,不過(guò)里面存儲(chǔ)的就是它監(jiān)聽(tīng)的pubsub_patterns的列表(就是sds)姨涡,而不是pubsubPattern結(jié)構(gòu)體。
當(dāng)用戶往一個(gè)頻道發(fā)送消息的時(shí)候吧慢,首先會(huì)在Redis server中的pubsub_channels里面查找該頻道涛漂,然后往它的客戶列表發(fā)送消息。然后在Redis server里面的pubsub_patterns里面查找匹配的模式检诗,然后往client里面發(fā)送消息匈仗。 這里并沒(méi)有去除重復(fù)的客戶锻弓,在pubsub_channels可能已經(jīng)給某一個(gè)client發(fā)過(guò)message了规揪,然后在pubsub_patterns中可能還會(huì)給用戶再發(fā)一次(甚至更多次)。 估計(jì)Redis認(rèn)為這是客戶程序自己的問(wèn)題坦康,所以不處理攻泼。
五火架、總結(jié)
總的來(lái)看鉴象,Redis比Memcached的功能多很多,實(shí)現(xiàn)也更復(fù)雜何鸡。 不過(guò)Memcached更專(zhuān)注于保存key-value數(shù)據(jù)(這已經(jīng)能滿足大多數(shù)使用場(chǎng)景了)炼列,而Redis提供更豐富的數(shù)據(jù)結(jié)構(gòu)及其他的一些功能。
不能說(shuō)Redis比Memcached好音比,只是從源碼閱讀的角度來(lái)看俭尖,Redis的價(jià)值或許更大一點(diǎn)。 另外洞翩,Redis 3.0里面支持了集群功能稽犁,這部分的代碼還沒(méi)有研究,后續(xù)再跟進(jìn)骚亿。