明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

memcached與redis完成的比較

[摘要]memcached和redis,作為近些年最常用的緩存服務(wù)器,相信大家對(duì)它們?cè)偈煜げ贿^(guò)了。前兩年還在學(xué)校時(shí),我曾經(jīng)讀過(guò)它們的主要源碼,如今寫篇筆記從個(gè)人角度簡(jiǎn)單對(duì)比一下它們的實(shí)現(xiàn)方式,權(quán)當(dāng)做復(fù)習(xí),有理解錯(cuò)誤之處,歡迎指正。文中使用的架構(gòu)類的圖片大多來(lái)自于網(wǎng)絡(luò),有部分圖與最新實(shí)現(xiàn)有出入,文中已經(jīng)指出...
memcached和redis,作為近些年最常用的緩存服務(wù)器,相信大家對(duì)它們?cè)偈煜げ贿^(guò)了。前兩年還在學(xué)校時(shí),我曾經(jīng)讀過(guò)它們的主要源碼,如今寫篇筆記從個(gè)人角度簡(jiǎn)單對(duì)比一下它們的實(shí)現(xiàn)方式,權(quán)當(dāng)做復(fù)習(xí),有理解錯(cuò)誤之處,歡迎指正。

文中使用的架構(gòu)類的圖片大多來(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寫一個(gè)網(wǎng)絡(luò)服務(wù)器,就因?yàn)橐3謋d和connect對(duì)應(yīng)關(guān)系,不想自己寫紅黑樹(shù),然后用了STL里面的set,導(dǎo)致項(xiàng)目變成了c++的,最后項(xiàng)目使用g++編譯,這事我不說(shuō)誰(shuí)知道?)。顯然這種方式只能針對(duì)connection數(shù)量上限已確定,并且不是太大的網(wǎng)絡(luò)服務(wù)器,像nginx這種http服務(wù)器就不適用,nginx就是自己寫了紅黑樹(shù)。

而memcached是多線程的,使用master-worker的方式,主線程監(jiān)聽(tīng)端口,建立連接,然后順序分配給各個(gè)工作線程。每一個(gè)從線程都有一個(gè)event loop,它們服務(wù)不同的客戶端。master線程和worker線程之間使用管道通信,每一個(gè)工作線程都會(huì)創(chuàng)建一個(gè)管道,然后保存寫端和讀端,并且將讀端加入event loop,監(jiān)聽(tīng)可讀事件。同時(shí),每個(gè)從線程都有一個(gè)就緒連接隊(duì)列,主線程連接連接后,將連接的item放入這個(gè)隊(duì)列,然后往該線程的管道的寫端寫入一個(gè)connect命令,這樣event loop中加入的管道讀端就會(huì)就緒,從線程讀取命令,解析命令發(fā)現(xiàn)是有連接,然后就會(huì)去自己的就緒隊(duì)列中獲取連接,并進(jìn)行處理。多線程的優(yōu)勢(shì)就是可以充分發(fā)揮多核的優(yōu)勢(shì),不過(guò)編寫程序麻煩一點(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的值。

memcached與redis實(shí)現(xiàn)的對(duì)比

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ò)容的操作,專門有一個(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)有使用。

memcached與redis實(shí)現(xiàn)的對(duì)比
memcached與redis實(shí)現(xiàn)的對(duì)比
memcached與redis實(shí)現(xiàn)的對(duì)比

如上圖,整個(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ì)象有類型,一共5種:字符串,鏈表,集合,有序集合,哈希表。 同時(shí),為了提高效率,redis為每種類型準(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ì)象類型的時(shí)候,再根據(jù)type來(lái)判斷。而且正式由于采用了這種面向?qū)ο蟮姆椒ǎ宺edis的代碼看起來(lái)很像c++代碼,其實(shí)全是用c寫的。

//#define REDIS_STRING 0    // 字符串類型
//#define REDIS_LIST 1        // 鏈表類型
//#define REDIS_SET 2        // 集合類型(無(wú)序的),可以求差集,并集等
//#define REDIS_ZSET 3        // 有序的集合類型
//#define REDIS_HASH 4        // 哈希類型

//#define REDIS_ENCODING_RAW 0     /* Raw representation */ //raw  未加工
//#define REDIS_ENCODING_INT 1     /* Encoded as integer */
//#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
//#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
//#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
//#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
//#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
//#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
//#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds 
                                                                     string encoding */

typedef struct redisObject {
    unsigned type:4;            // 對(duì)象的類型,包括 /* Object types */
    unsigned encoding:4;        // 底部為了節(jié)省空間,一種type的數(shù)據(jù),
                                                // 可   以采用不同的存儲(chǔ)方式
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;         // 引用計(jì)數(shù)
    void *ptr;
} robj;

說(shuō)到底redis還是一個(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)度。

struct sdshdr {
    int len;
    int free;
    char buf[];
};

字符串解決了,所有的key都存成sds就行了,那么key和value怎么關(guān)聯(lián)呢?key-value的格式在腳本語(yǔ)言中很好處理,直接使用字典即可,C沒(méi)有字典,怎么辦呢?自己寫一個(gè)唄(redis十分熱衷于造輪子)?聪旅娴拇a,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類似的做法,也是使用開(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)稱為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)銷均分到用戶每個(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

typedef struct dict {
    dictType *type;    // 哈希表的相關(guān)屬性
    void *privdata;    // 額外信息
    dictht ht[2];    // 兩張哈希表,分主和副,用于擴(kuò)容
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 記錄當(dāng)前數(shù)據(jù)遷移的位置,在擴(kuò)容的時(shí)候用的
    int iterators; /* number of iterators currently running */    // 目前存在的迭代器的數(shù)量
} dict;

typedef struct dictht {
    dictEntry **table;  // dictEntry是item,多個(gè)item組成hash桶里面的鏈表,table則是多個(gè)鏈表頭指針組成的數(shù)組的指針
    unsigned long size;    // 這個(gè)就是桶的數(shù)量
    // sizemask取size - 1, 然后一個(gè)數(shù)據(jù)來(lái)的時(shí)候,通過(guò)計(jì)算出的hashkey, 讓hashkey & sizemask來(lái)確定它要放的桶的位置
    // 當(dāng)size取2^n的時(shí)候,sizemask就是1...111,這樣就和hashkey % size有一樣的效果,但是使用&會(huì)快很多。這就是原因
    unsigned long sizemask;  
    unsigned long used;        // 已經(jīng)數(shù)值的dictEntry數(shù)量
} dictht;

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);     // hash的方法
    void *(*keyDup)(void *privdata, const void *key);    // key的復(fù)制方法
    void *(*valDup)(void *privdata, const void *obj);    // value的復(fù)制方法
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);    // key之間的比較
    void (*keyDestructor)(void *privdata, void *key);    // key的析構(gòu)
    void (*valDestructor)(void *privdata, void *obj);    // value的析構(gòu)
} dictType;

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next;
} dictEntry;

有了dict,數(shù)據(jù)庫(kù)就好實(shí)現(xiàn)了。所有數(shù)據(jù)讀存儲(chǔ)在dict中,key存儲(chǔ)成dictEntry中的key(string),用void* 指向一個(gè)redis object,它可以是5種類型中的任何一種。如下圖,結(jié)構(gòu)構(gòu)造是這樣,不過(guò)這個(gè)圖已經(jīng)過(guò)時(shí)了,有一些與redis3.0不符合的地方。

memcached與redis實(shí)現(xiàn)的對(duì)比

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)換,其余類型的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)類似,所以當(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)方式,這也是這樣使用類似面向?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?蛻艨梢赃x擇使用哪一個(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類似,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)題。

以上就是redis的數(shù)據(jù)的實(shí)現(xiàn),與memcached不同,redis還支持?jǐn)?shù)據(jù)持久化,這個(gè)下面介紹。

4.redis數(shù)據(jù)庫(kù)持久化

redis和memcached的最大不同,就是redis支持?jǐn)?shù)據(jù)持久化,這也是很多人選擇使用redis而不是memcached的最大原因。 redis的持久化,分為兩種策略,用戶可以配置使用不同的策略。

4.1 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ù)。

memcached與redis實(shí)現(xiàn)的對(duì)比

每一個(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ù)了。

memcached與redis實(shí)現(xiàn)的對(duì)比

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                        long long expiretime, long long now)
{
    /* Save the expire time */
    if (expiretime != -1) {
        /* If this key is already expired skip it */
        if (expiretime < now) return 0;
        if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
    }

    /* Save type, key, value */
    if (rdbSaveObjectType(rdb,val) == -1) return -1;
    if (rdbSaveStringObject(rdb,key) == -1) return -1;
    if (rdbSaveObject(rdb,val) == -1) return -1;
    return 1;
}

由上面的代碼也可以看出,存儲(chǔ)的時(shí)候,先檢查expire time,如果已經(jīng)過(guò)期,不存就行了,否則,則將expire time存下來(lái),注意,及時(shí)是存儲(chǔ)expire time,也是先存儲(chǔ)它的類型為REDIS_RDB_OPCODE_EXPIRETIME_MS,然后再存儲(chǔ)具體過(guò)期時(shí)間。接下來(lái)存儲(chǔ)真正的key-value對(duì),首先存儲(chǔ)value的類型,然后存儲(chǔ)key(它按照字符串存儲(chǔ)),然后存儲(chǔ)value,如下圖。

memcached與redis實(shí)現(xiàn)的對(duì)比

在rdbsaveobject中,會(huì)根據(jù)val的不同類型,按照不同的方式存儲(chǔ),不過(guò)從根本上來(lái)看,最終都是轉(zhuǎn)換成字符串存儲(chǔ),比如val是一個(gè)linklist,那么先存儲(chǔ)整個(gè)list的字節(jié)數(shù),然后遍歷這個(gè)list,把數(shù)據(jù)取出來(lái),依次按照string寫入文件。對(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的類型,然后存儲(chǔ)key(字符串方式),然后根據(jù)value的類型和底層實(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的具體類型,實(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ù)寫入一個(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效率不行,而且不好管理。

4.2 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中的命令寫入aof文件(其實(shí)是write,真正寫入的是內(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ù)寫入了內(nèi)核緩沖區(qū),真正寫入文件時(shí)內(nèi)核自己決定的,可能需要延后一段時(shí)間。 不過(guò)redis支持配置,可以配置每次寫入后sync,則在redis里面調(diào)用sync,將內(nèi)核中的數(shù)據(jù)寫入文件,這不過(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)就寫入了文件中。

再來(lái)看,如果不想使用aof_buf保存每次的修改命令,也可以使用aof持久化。redis提供aof_rewrite,即根據(jù)現(xiàn)有的數(shù)據(jù)庫(kù)生成命令,然后把命令寫入aof文件中。很奇特吧?對(duì),就是這么厲害。進(jìn)行aof_rewrite的時(shí)候,redis變量每個(gè)數(shù)據(jù)庫(kù),然后根據(jù)key-value對(duì)中value的具體類型,生成不同的命令,比如是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ù)寫入一個(gè)臨時(shí)文件,然后寫完后用新號(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里面的客戶端的信息,里面有寫和讀的緩沖區(qū),它是存在于redis服務(wù)器中的。所以,如下圖,直接讀入aof的命令,放入客戶端的讀緩沖區(qū)中,然后執(zhí)行這個(gè)客戶端的命令即可。這樣就完成了aof文件的載入。

// 創(chuàng)建偽客戶端
fakeClient = createFakeClient();

while(命令不為空) {
   // 獲取一條命令的參數(shù)信息 argc, argv
   ...

    // 執(zhí)行
    fakeClient->argc = argc;
    fakeClient->argv = argv;
    cmd->proc(fakeClient);
}

整個(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)必要了。

memcached與redis實(shí)現(xiàn)的對(duì)比

我們知道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是頻道的名稱(顯然要唯一了),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)體。

typedef struct pubsubPattern {
    redisClient *client;    // 監(jiān)聽(tīng)的client
    robj *pattern;            // 模式
} pubsubPattern;

當(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)題,所以不處理。

/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

/* Send to clients listening for that channel */
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
            redisClient *c = ln->value;

            addReply(c,shared.mbulkhdr[3]);
            addReply(c,shared.messagebulk);
            addReplyBulk(c,channel);
            addReplyBulk(c,message);
            receivers++;
        }
    }
 /* Send to clients listening to matching channels */
    if (listLength(server.pubsub_patterns)) {
        listRewind(server.pubsub_patterns,&li);
        channel = getDecodedObject(channel);
        while ((ln = listNext(&li)) != NULL) {
            pubsubPattern *pat = ln->value;

            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {
                addReply(pat->client,shared.mbulkhdr[4]);
                addReply(pat->client,shared.pmessagebulk);
                addReplyBulk(pat->client,pat->pattern);
                addReplyBulk(pat->client,channel);
                addReplyBulk(pat->client,message);
                receivers++;
            }
        }
        decrRefCount(channel);
    }
    return receivers;
}

六. 總結(jié)

總的來(lái)看,redis比memcached的功能多很多,實(shí)現(xiàn)也更復(fù)雜。 不過(guò)memcached更專注于保存key-value數(shù)據(jù)(這已經(jīng)能滿足大多數(shù)使用場(chǎng)景了),而redis提供更豐富的數(shù)據(jù)結(jié)構(gòu)及其他的一些功能。不能說(shuō)redis比memcached好,不過(guò)從源碼閱讀的角度來(lái)看,redis的價(jià)值或許更大一點(diǎn)。 另外,redis3.0里面支持了集群功能,這部分的代碼還沒(méi)有研究,后續(xù)再跟進(jìn)。

以上就是memcached與redis實(shí)現(xiàn)的對(duì)比的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!


學(xué)習(xí)教程快速掌握從入門到精通的SQL知識(shí)。