本篇文章給大家?guī)?lái)的內(nèi)容是關(guān)于redis與memcached有何區(qū)別 ?redis和memcached的區(qū)別比較,有一定的參考價(jià)值,有需要的朋友可以參考一下,希望對(duì)你有所幫助。
memcached和redis,作為近些年最常用的緩存服務(wù)器,相信大家對(duì)它們?cè)偈煜げ贿^(guò)了。前兩年還在學(xué)校時(shí),我曾經(jīng)讀過(guò)它們的主要源碼,如今寫(xiě)篇筆記從個(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寫(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ò)容的操作,專門(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ù)。