摘要:它們都使用來做事件循環(huán),不過是單線程的服務器也是多線程的,只不過除了主線程以外,其他線程沒有,只是會進行一些后臺存儲工作,而是多線程的。支持設置過期時間,即,但是內部并不定期檢查數據是否過期,而是客戶進程使用該數據的時候,會
歡迎大家前往騰訊云+社區(qū),獲取更多騰訊海量技術實踐干貨哦~
本文由Super發(fā)表于云+社區(qū)專欄
memcached和redis,作為近些年最常用的緩存服務器,相信大家對它們再熟悉不過了。前兩年還在學校時,我曾經讀過它們的主要源碼,如今寫篇筆記從個人角度簡單對比一下它們的實現(xiàn)方式,權當做復習,有理解錯誤之處,歡迎指正。
文中使用的架構類的圖片大多來自于網絡,有部分圖與最新實現(xiàn)有出入,文中已經指出。
一. 綜述讀一個軟件的源碼,首先要弄懂軟件是用作干什么的,那memcached和redis是干啥的?眾所周知,數據一般會放在數據庫中,但是查詢數據會相對比較慢,特別是用戶很多時,頻繁的查詢,需要耗費大量的時間。怎么辦呢?數據放在哪里查詢快?那肯定是內存中。memcached和redis就是將數據存儲在內存中,按照key-value的方式查詢,可以大幅度提高效率。所以一般它們都用做緩存服務器,緩存常用的數據,需要查詢的時候,直接從它們那兒獲取,減少查詢數據庫的次數,提高查詢效率。
二. 服務方式memcached和redis怎么提供服務呢?它們是獨立的進程,需要的話,還可以讓他們變成daemon進程,所以我們的用戶進程要使用memcached和redis的服務的話,就需要進程間通信了??紤]到用戶進程和memcached和redis不一定在同一臺機器上,所以還需要支持網絡間通信。因此,memcached和redis自己本身就是網絡服務器,用戶進程通過與他們通過網絡來傳輸數據,顯然最簡單和最常用的就是使用tcp連接了。另外,memcached和redis都支持udp協(xié)議。而且當用戶進程和memcached和redis在同一機器時,還可以使用unix域套接字通信。
三. 事件模型下面開始講他們具體是怎么實現(xiàn)的了。首先來看一下它們的事件模型。
自從epoll出來以后,幾乎所有的網絡服務器全都拋棄select和poll,換成了epoll。redis也一樣,只不多它還提供對select和poll的支持,可以自己配置使用哪一個,但是一般都是用epoll。另外針對BSD,還支持使用kqueue。而memcached是基于libevent的,不過libevent底層也是使用epoll的,所以可以認為它們都是使用epoll。epoll的特性這里就不介紹了,網上介紹文章很多。
它們都使用epoll來做事件循環(huán),不過redis是單線程的服務器(redis也是多線程的,只不過除了主線程以外,其他線程沒有event loop,只是會進行一些后臺存儲工作),而memcached是多線程的。 redis的事件模型很簡單,只有一個event loop,是簡單的reactor實現(xiàn)。不過redis事件模型中有一個亮點,我們知道epoll是針對fd的,它返回的就緒事件也是只有fd,redis里面的fd就是服務器與客戶端連接的socket的fd,但是處理的時候,需要根據這個fd找到具體的客戶端的信息,怎么找呢?通常的處理方式就是用紅黑樹將fd與客戶端信息保存起來,通過fd查找,效率是lgn。不過redis比較特殊,redis的客戶端的數量上限可以設置,即可以知道同一時刻,redis所打開的fd的上限,而我們知道,進程的fd在同一時刻是不會重復的(fd只有關閉后才能復用),所以redis使用一個數組,將fd作為數組的下標,數組的元素就是客戶端的信息,這樣,直接通過fd就能定位客戶端信息,查找效率是O(1),還省去了復雜的紅黑樹的實現(xiàn)(我曾經用c寫一個網絡服務器,就因為要保持fd和connect對應關系,不想自己寫紅黑樹,然后用了STL里面的set,導致項目變成了c++的,最后項目使用g++編譯,這事我不說誰知道?)。顯然這種方式只能針對connection數量上限已確定,并且不是太大的網絡服務器,像nginx這種http服務器就不適用,nginx就是自己寫了紅黑樹。
而memcached是多線程的,使用master-worker的方式,主線程監(jiān)聽端口,建立連接,然后順序分配給各個工作線程。每一個從線程都有一個event loop,它們服務不同的客戶端。master線程和worker線程之間使用管道通信,每一個工作線程都會創(chuàng)建一個管道,然后保存寫端和讀端,并且將讀端加入event loop,監(jiān)聽可讀事件。同時,每個從線程都有一個就緒連接隊列,主線程連接連接后,將連接的item放入這個隊列,然后往該線程的管道的寫端寫入一個connect命令,這樣event loop中加入的管道讀端就會就緒,從線程讀取命令,解析命令發(fā)現(xiàn)是有連接,然后就會去自己的就緒隊列中獲取連接,并進行處理。多線程的優(yōu)勢就是可以充分發(fā)揮多核的優(yōu)勢,不過編寫程序麻煩一點,memcached里面就有各種鎖和條件變量來進行線程同步。
四. 內存分配memcached和redis的核心任務都是在內存中操作數據,內存管理自然是核心的內容。
首先看看他們的內存分配方式。memcached是有自己得內存池的,即預先分配一大塊內存,然后接下來分配內存就從內存池中分配,這樣可以減少內存分配的次數,提高效率,這也是大部分網絡服務器的實現(xiàn)方式,只不過各個內存池的管理方式根據具體情況而不同。而redis沒有自己得內存池,而是直接使用時分配,即什么時候需要什么時候分配,內存管理的事交給內核,自己只負責取和釋放(redis既是單線程,又沒有自己的內存池,是不是感覺實現(xiàn)的太簡單了?那是因為它的重點都放在數據庫模塊了)。不過redis支持使用tcmalloc來替換glibc的malloc,前者是google的產品,比glibc的malloc快。
由于redis沒有自己的內存池,所以內存申請和釋放的管理就簡單很多,直接malloc和free即可,十分方便。而memcached是支持內存池的,所以內存申請是從內存池中獲取,而free也是還給內存池,所以需要很多額外的管理操作,實現(xiàn)起來麻煩很多,具體的會在后面memcached的slab機制講解中分析。
五. 數據庫實現(xiàn)接下來看看他們的最核心內容,各自數據庫的實現(xiàn)。
1. memcached數據庫實現(xiàn)memcached只支持key-value,即只能一個key對于一個value。它的數據在內存中也是這樣以key-value對的方式存儲,它使用slab機制。
首先看memcached是如何存儲數據的,即存儲key-value對。如下圖,每一個key-value對都存儲在一個item結構中,包含了相關的屬性和key和value的值。
item是保存key-value對的,當item多的時候,怎么查找特定的item是個問題。所以memcached維護了一個hash表,它用于快速查找item。hash表適用開鏈法(與redis一樣)解決鍵的沖突,每一個hash表的桶里面存儲了一個鏈表,鏈表節(jié)點就是item的指針,如上圖中的h_next就是指桶里面的鏈表的下一個節(jié)點。 hash表支持擴容(item的數量是桶的數量的1.5以上時擴容),有一個primary_hashtable,還有一個old_hashtable,其中正常適用primary_hashtable,但是擴容的時候,將old_hashtable = primary_hashtable,然后primary_hashtable設置為新申請的hash表(桶的數量乘以2),然后依次將old_hashtable 里面的數據往新的hash表里面移動,并用一個變量expand_bucket記錄以及移動了多少個桶,移動完成后,再free原來的old_hashtable 即可(redis也是有兩個hash表,也是移動,不過不是后臺線程完成,而是每次移動一個桶)。擴容的操作,專門有一個后臺擴容的線程來完成,需要擴容的時候,使用條件變量通知它,完成擴容后,它又考試阻塞等待擴容的條件變量。這樣在擴容的時候,查找一個item可能會在primary_hashtable和old_hashtable的任意一個中,需要根據比較它的桶的位置和expand_bucket的大小來比較確定它在哪個表里。
item是從哪里分配的呢?從slab中。如下圖,memcached有很多slabclass,它們管理slab,每一個slab其實是trunk的集合,真正的item是在trunk中分配的,一個trunk分配一個item。一個slab中的trunk的大小一樣,不同的slab,trunk的大小按比例遞增,需要新申請一個item的時候,根據它的大小來選擇trunk,規(guī)則是比它大的最小的那個trunk。這樣,不同大小的item就分配在不同的slab中,歸不同的slabclass管理。 這樣的缺點是會有部分內存浪費,因為一個trunk可能比item大,如圖2,分配100B的item的時候,選擇112的trunk,但是會有12B的浪費,這部分內存資源沒有使用。
如上圖,整個構造就是這樣,slabclass管理slab,一個slabclass有一個slab_list,可以管理多個slab,同一個slabclass中的slab的trunk大小都一樣。slabclass有一個指針slot,保存了未分配的item已經被free掉的item(不是真的free內存,只是不用了而已),有item不用的時候,就放入slot的頭部,這樣每次需要在當前slab中分配item的時候,直接取slot取即可,不用管item是未分配過的還是被釋放掉的。
然后,每一個slabclass對應一個鏈表,有head數組和tail數組,它們分別保存了鏈表的頭節(jié)點和尾節(jié)點。鏈表中的節(jié)點就是改slabclass所分配的item,新分配的放在頭部,鏈表越往后的item,表示它已經很久沒有被使用了。當slabclass的內存不足,需要刪除一些過期item的時候,就可以從鏈表的尾部開始刪除,沒錯,這個鏈表就是為了實現(xiàn)LRU。光靠它還不行,因為鏈表的查詢是O(n)的,所以定位item的時候,使用hash表,這已經有了,所有分配的item已經在hash表中了,所以,hash用于查找item,然后鏈表有用存儲item的最近使用順序,這也是lru的標準實現(xiàn)方法。
每次需要新分配item的時候,找到slabclass對于的鏈表,從尾部往前找,看item是否已經過期,過期的話,直接就用這個過期的item當做新的item。沒有過期的,則需要從slab中分配trunk,如果slab用完了,則需要往slabclass中添加slab了。
memcached支持設置過期時間,即expire time,但是內部并不定期檢查數據是否過期,而是客戶進程使用該數據的時候,memcached會檢查expire time,如果過期,直接返回錯誤。這樣的優(yōu)點是,不需要額外的cpu來進行expire time的檢查,缺點是有可能過期數據很久不被使用,則一直沒有被釋放,占用內存。
memcached是多線程的,而且只維護了一個數據庫,所以可能有多個客戶進程操作同一個數據,這就有可能產生問題。比如,A已經把數據更改了,然后B也更改了改數據,那么A的操作就被覆蓋了,而可能A不知道,A任務數據現(xiàn)在的狀態(tài)時他改完后的那個值,這樣就可能產生問題。為了解決這個問題,memcached使用了CAS協(xié)議,簡單說就是item保存一個64位的unsigned int值,標記數據的版本,每更新一次(數據值有修改),版本號增加,然后每次對數據進行更改操作,需要比對客戶進程傳來的版本號和服務器這邊item的版本號是否一致,一致則可進行更改操作,否則提示臟數據。
以上就是memcached如何實現(xiàn)一個key-value的數據庫的介紹。
2. redis數據庫實現(xiàn)首先redis數據庫的功能強大一些,因為不像memcached只支持保存字符串,redis支持string, list, set,sorted set,hash table 5種數據結構。例如存儲一個人的信息就可以使用hash table,用人的名字做key,然后name super, age 24, 通過key 和 name,就可以取到名字super,或者通過key和age,就可以取到年齡24。這樣,當只需要取得age的時候,不需要把人的整個信息取回來,然后從里面找age,直接獲取age即可,高效方便。
為了實現(xiàn)這些數據結構,redis定義了抽象的對象redis object,如下圖。每一個對象有類型,一共5種:字符串,鏈表,集合,有序集合,哈希表。 同時,為了提高效率,redis為每種類型準備了多種實現(xiàn)方式,根據特定的場景來選擇合適的實現(xiàn)方式,encoding就是表示對象的實現(xiàn)方式的。然后還有記錄了對象的lru,即上次被訪問的時間,同時在redis 服務器中會記錄一個當前的時間(近似值,因為這個時間只是每隔一定時間,服務器進行自動維護的時候才更新),它們兩個只差就可以計算出對象多久沒有被訪問了。 然后redis object中還有引用計數,這是為了共享對象,然后確定對象的刪除時間用的。最后使用一個void*指針來指向對象的真正內容。正式由于使用了抽象redis object,使得數據庫操作數據時方便很多,全部統(tǒng)一使用redis object對象即可,需要區(qū)分對象類型的時候,再根據type來判斷。而且正式由于采用了這種面向對象的方法,讓redis的代碼看起來很像c++代碼,其實全是用c寫的。
//#define REDIS_STRING 0 // 字符串類型 //#define REDIS_LIST 1 // 鏈表類型 //#define REDIS_SET 2 // 集合類型(無序的),可以求差集,并集等 //#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; // 對象的類型,包括 /* Object types */ unsigned encoding:4; // 底部為了節(jié)省空間,一種type的數據, // 可 以采用不同的存儲方式 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; // 引用計數 void *ptr; } robj;
說到底redis還是一個key-value的數據庫,不管它支持多少種數據結構,最終存儲的還是以key-value的方式,只不過value可以是鏈表,set,sorted set,hash table等。和memcached一樣,所有的key都是string,而set,sorted set,hash table等具體存儲的時候也用到了string。 而c沒有現(xiàn)成的string,所以redis的首要任務就是實現(xiàn)一個string,取名叫sds(simple dynamic string),如下的代碼, 非常簡單的一個結構體,len存儲改string的內存總長度,free表示還有多少字節(jié)沒有使用,而buf存儲具體的數據,顯然len-free就是目前字符串的長度。
struct sdshdr { int len; int free; char buf[]; };
字符串解決了,所有的key都存成sds就行了,那么key和value怎么關聯(lián)呢?key-value的格式在腳本語言中很好處理,直接使用字典即可,C沒有字典,怎么辦呢?自己寫一個唄(redis十分熱衷于造輪子)??聪旅娴拇a,privdata存額外信息,用的很少,至少我們發(fā)現(xiàn)。 dictht是具體的哈希表,一個dict對應兩張哈希表,這是為了擴容(包括rehashidx也是為了擴容)。dictType存儲了哈希表的屬性。redis還為dict實現(xiàn)了迭代器(所以說看起來像c++代碼)。
哈希表的具體實現(xiàn)是和mc類似的做法,也是使用開鏈法來解決沖突,不過里面用到了一些小技巧。比如使用dictType存儲函數指針,可以動態(tài)配置桶里面元素的操作方法。又比如dictht中保存的sizemask取size(桶的數量)-1,用它與key做&操作來代替取余運算,加快速度等等??偟膩砜?,dict里面有兩個哈希表,每個哈希表的桶里面存儲dictEntry鏈表,dictEntry存儲具體的key和value。
前面說過,一個dict對于兩個dictht,是為了擴容(其實還有縮容)。正常的時候,dict只使用dictht[0],當dict[0]中已有entry的數量與桶的數量達到一定的比例后,就會觸發(fā)擴容和縮容操作,我們統(tǒng)稱為rehash,這時,為dictht[1]申請rehash后的大小的內存,然后把dictht[0]里的數據往dictht[1]里面移動,并用rehashidx記錄當前已經移動萬的桶的數量,當所有桶都移完后,rehash完成,這時將dictht[1]變成dictht[0], 將原來的dictht[0]變成dictht[1],并變?yōu)閚ull即可。不同于memcached,這里不用開一個后臺線程來做,而是就在event loop中完成,并且rehash不是一次性完成,而是分成多次,每次用戶操作dict之前,redis移動一個桶的數據,直到rehash完成。這樣就把移動分成多個小移動完成,把rehash的時間開銷均分到用戶每個操作上,這樣避免了用戶一個請求導致rehash的時候,需要等待很長時間,直到rehash完成才有返回的情況。不過在rehash期間,每個操作都變慢了點,而且用戶還不知道redis在他的請求中間添加了移動數據的操作,感覺redis太賤了 :-D
typedef struct dict { dictType *type; // 哈希表的相關屬性 void *privdata; // 額外信息 dictht ht[2]; // 兩張哈希表,分主和副,用于擴容 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 記錄當前數據遷移的位置,在擴容的時候用的 int iterators; /* number of iterators currently running */ // 目前存在的迭代器的數量 } dict; typedef struct dictht { dictEntry **table; // dictEntry是item,多個item組成hash桶里面的鏈表,table則是多個鏈表頭指針組成的數組的指針 unsigned long size; // 這個就是桶的數量 // sizemask取size - 1, 然后一個數據來的時候,通過計算出的hashkey, 讓hashkey & sizemask來確定它要放的桶的位置 // 當size取2^n的時候,sizemask就是1...111,這樣就和hashkey % size有一樣的效果,但是使用&會快很多。這就是原因 unsigned long sizemask; unsigned long used; // 已經數值的dictEntry數量 } dictht; typedef struct dictType { unsigned int (*hashFunction)(const void *key); // hash的方法 void *(*keyDup)(void *privdata, const void *key); // key的復制方法 void *(*valDup)(void *privdata, const void *obj); // value的復制方法 int (*keyCompare)(void *privdata, const void *key1, const void *key2); // key之間的比較 void (*keyDestructor)(void *privdata, void *key); // key的析構 void (*valDestructor)(void *privdata, void *obj); // value的析構 } dictType; typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; } v; struct dictEntry *next; } dictEntry;
有了dict,數據庫就好實現(xiàn)了。所有數據讀存儲在dict中,key存儲成dictEntry中的key(string),用void* 指向一個redis object,它可以是5種類型中的任何一種。如下圖,結構構造是這樣,不過這個圖已經過時了,有一些與redis3.0不符合的地方。
5中type的對象,每一個都至少有兩種底層實現(xiàn)方式。string有3種:REDIS_ENCODING_RAW, REDIS_ENCIDING_INT, REDIS_ENCODING_EMBSTR, list有:普通雙向鏈表和壓縮鏈表,壓縮鏈表簡單的說,就是講數組改造成鏈表,連續(xù)的空間,然后通過存儲字符串的大小信息來模擬鏈表,相對普通鏈表來說可以節(jié)省空間,不過有副作用,由于是連續(xù)的空間,所以改變內存大小的時候,需要重新分配,并且由于保存了字符串的字節(jié)大小,所有有可能引起連續(xù)更新(具體實現(xiàn)請詳細看代碼)。set有dict和intset(全是整數的時候使用它來存儲), sorted set有:skiplist和ziplist, hashtable實現(xiàn)有壓縮列表和dict和ziplist。skiplist就是跳表,它有接近于紅黑樹的效率,但是實現(xiàn)起來比紅黑樹簡單很多,所以被采用(奇怪,這里又不造輪子了,難道因為這個輪子有點難?)。 hash table可以使用dict實現(xiàn),則改dict中,每個dictentry中key保存了key(這是哈希表中的鍵值對的key),而value則保存了value,它們都是string。 而set中的dict,每個dictentry中key保存了set中具體的一個元素的值,value則為null。圖中的zset(有序集合)有誤,zset使用skiplist和ziplist實現(xiàn),首先skiplist很好理解,就把它當做紅黑樹的替代品就行,和紅黑樹一樣,它也可以排序。怎么用ziplist存儲zset呢?首先在zset中,每個set中的元素都有一個分值score,用它來排序。所以在ziplist中,按照分值大小,先存元素,再存它的score,再存下一個元素,然后score。這樣連續(xù)存儲,所以插入或者刪除的時候,都需要重新分配內存。所以當元素超過一定數量,或者某個元素的字符數超過一定數量,redis就會選擇使用skiplist來實現(xiàn)zset(如果當前使用的是ziplist,會將這個ziplist中的數據取出,存入一個新的skiplist,然后刪除改ziplist,這就是底層實現(xiàn)轉換,其余類型的redis object也是可以轉換的)。 另外,ziplist如何實現(xiàn)hashtable呢?其實也很簡單,就是存儲一個key,存儲一個value,再存儲一個key,再存儲一個value。還是順序存儲,與zset實現(xiàn)類似,所以當元素超過一定數量,或者某個元素的字符數超過一定數量時,就會轉換成hashtable來實現(xiàn)。各種底層實現(xiàn)方式是可以轉換的,redis可以根據情況選擇最合適的實現(xiàn)方式,這也是這樣使用類似面向對象的實現(xiàn)方式的好處。
需要指出的是,使用skiplist來實現(xiàn)zset的時候,其實還用了一個dict,這個dict存儲一樣的鍵值對。為什么呢?因為skiplist的查找只是lgn的(可能變成n),而dict可以到O(1), 所以使用一個dict來加速查找,由于skiplist和dict可以指向同一個redis object,所以不會浪費太多內存。另外使用ziplist實現(xiàn)zset的時候,為什么不用dict來加速查找呢?因為ziplist支持的元素個數很少(個數多時就轉換成skiplist了),順序遍歷也很快,所以不用dict了。
這樣看來,上面的dict,dictType,dictHt,dictEntry,redis object都是很有考量的,它們配合實現(xiàn)了一個具有面向對象色彩的靈活、高效數據庫。不得不說,redis數據庫的設計還是很厲害的。
與memcached不同的是,redis的數據庫不止一個,默認就有16個,編號0-15。客戶可以選擇使用哪一個數據庫,默認使用0號數據庫。 不同的數據庫數據不共享,即在不同的數據庫中可以存在同樣的key,但是在同一個數據庫中,key必須是唯一的。
redis也支持expire time的設置,我們看上面的redis object,里面沒有保存expire的字段,那redis怎么記錄數據的expire time呢? redis是為每個數據庫又增加了一個dict,這個dict叫expire dict,它里面的dict entry里面的key就是數對的key,而value全是數據為64位int的redis object,這個int就是expire time。這樣,判斷一個key是否過期的時候,去expire dict里面找到它,取出expire time比對當前時間即可。為什么這樣做呢? 因為并不是所有的key都會設置過期時間,所以,對于不設置expire time的key來說,保存一個expire time會浪費空間,而是用expire dict來多帶帶保存的話,可以根據需要靈活使用內存(檢測到key過期時,會把它從expire dict中刪除)。
redis的expire 機制是怎樣的呢? 與memcahed類似,redis也是惰性刪除,即要用到數據時,先檢查key是否過期,過期則刪除,然后返回錯誤。單純的靠惰性刪除,上面說過可能會導致內存浪費,所以redis也有補充方案,redis里面有個定時執(zhí)行的函數,叫servercron,它是維護服務器的函數,在它里面,會對過期數據進行刪除,注意不是全刪,而是在一定的時間內,對每個數據庫的expire dict里面的數據隨機選取出來,如果過期,則刪除,否則再選,直到規(guī)定的時間到。即隨機選取過期的數據刪除,這個操作的時間分兩種,一種較長,一種較短,一般執(zhí)行短時間的刪除,每隔一定的時間,執(zhí)行一次長時間的刪除。這樣可以有效的緩解光采用惰性刪除而導致的內存浪費問題。
以上就是redis的數據的實現(xiàn),與memcached不同,redis還支持數據持久化,這個下面介紹。
4.redis數據庫持久化redis和memcached的最大不同,就是redis支持數據持久化,這也是很多人選擇使用redis而不是memcached的最大原因。 redis的持久化,分為兩種策略,用戶可以配置使用不同的策略。
4.1 RDB持久化 用戶執(zhí)行save或者bgsave的時候,就會觸發(fā)RDB持久化操作。RDB持久化操作的核心思想就是把數據庫原封不動的保存在文件里。
那如何存儲呢?如下圖, 首先存儲一個REDIS字符串,起到驗證的作用,表示是RDB文件,然后保存redis的版本信息,然后是具體的數據庫,然后存儲結束符EOF,最后用檢驗和。關鍵就是databases,看它的名字也知道,它存儲了多個數據庫,數據庫按照編號順序存儲,0號數據庫存儲完了,才輪到1,然后是2, 一直到最后一個數據庫。
每一個數據庫存儲方式如下,首先一個1字節(jié)的常量SELECTDB,表示切換db了,然后下一個接上數據庫的編號,它的長度是可變的,然后接下來就是具體的key-value對的數據了。
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; }
由上面的代碼也可以看出,存儲的時候,先檢查expire time,如果已經過期,不存就行了,否則,則將expire time存下來,注意,及時是存儲expire time,也是先存儲它的類型為REDIS_RDB_OPCODE_EXPIRETIME_MS,然后再存儲具體過期時間。接下來存儲真正的key-value對,首先存儲value的類型,然后存儲key(它按照字符串存儲),然后存儲value,如下圖。
在rdbsaveobject中,會根據val的不同類型,按照不同的方式存儲,不過從根本上來看,最終都是轉換成字符串存儲,比如val是一個linklist,那么先存儲整個list的字節(jié)數,然后遍歷這個list,把數據取出來,依次按照string寫入文件。對于hash table,也是先計算字節(jié)數,然后依次取出hash table中的dictEntry,按照string的方式存儲它的key和value,然后存儲下一個dictEntry。 總之,RDB的存儲方式,對一個key-value對,會先存儲expire time(如果有的話),然后是value的類型,然后存儲key(字符串方式),然后根據value的類型和底層實現(xiàn)方式,將value轉換成字符串存儲。這里面為了實現(xiàn)數據壓縮,以及能夠根據文件恢復數據,redis使用了很多編碼的技巧,有些我也沒太看懂,不過關鍵還是要理解思想,不要在意這些細節(jié)。
保存了RDB文件,當redis再啟動的時候,就根據RDB文件來恢復數據庫。由于以及在RDB文件中保存了數據庫的號碼,以及它包含的key-value對,以及每個key-value對中value的具體類型,實現(xiàn)方式,和數據,redis只要順序讀取文件,然后恢復object即可。由于保存了expire time,發(fā)現(xiàn)當前的時間已經比expire time大了,即數據已經超時了,則不恢復這個key-value對即可。
保存RDB文件是一個很巨大的工程,所以redis還提供后臺保存的機制。即執(zhí)行bgsave的時候,redis fork出一個子進程,讓子進程來執(zhí)行保存的工作,而父進程繼續(xù)提供redis正常的數據庫服務。由于子進程復制了父進程的地址空間,即子進程擁有父進程fork時的數據庫,子進程執(zhí)行save的操作,把它從父進程那兒繼承來的數據庫寫入一個temp文件即可。在子進程復制期間,redis會記錄數據庫的修改次數(dirty)。當子進程完成時,發(fā)送給父進程SIGUSR1信號,父進程捕捉到這個信號,就知道子進程完成了復制,然后父進程將子進程保存的temp文件改名為真正的rdb文件(即真正保存成功了才改成目標文件,這才是保險的做法)。然后記錄下這一次save的結束時間。
這里有一個問題,在子進程保存期間,父進程的數據庫已經被修改了,而父進程只是記錄了修改的次數(dirty),被沒有進行修正操作。似乎使得RDB保存的不是實時的數據庫,有點不太高大上的樣子。 不過后面要介紹的AOF持久化,就解決了這個問題。
除了客戶執(zhí)行sava或者bgsave命令,還可以配置RDB保存條件。即在配置文件中配置,在t時間內,數據庫被修改了dirty次,則進行后臺保存。redis在serve cron的時候,會根據dirty數目和上次保存的時間,來判斷是否符合條件,符合條件的話,就進行bg save,注意,任意時刻只能有一個子進程來進行后臺保存,因為保存是個很費io的操作,多個進程大量io效率不行,而且不好管理。
4.2 AOF持久化 首先想一個問題,保存數據庫一定需要像RDB那樣把數據庫里面的所有數據保存下來么?有沒有別的方法?
RDB保存的只是最終的數據庫,它是一個結果。結果是怎么來的?是通過用戶的各個命令建立起來的,所以可以不保存結果,而只保存建立這個結果的命令。 redis的AOF就是這個思想,它不同RDB保存db的數據,它保存的是一條一條建立數據庫的命令。
我們首先來看AOF文件的格式,它里面保存的是一條一條的命令,首先存儲命令長度,然后存儲命令,具體的分隔符什么的可以自己深入研究,這都不是重點,反正知道AOF文件存儲的是redis客戶端執(zhí)行的命令即可。
redis server中有一個sds aof_buf, 如果aof持久化打開的話,每個修改數據庫的命令都會存入這個aof_buf(保存的是aof文件中命令格式的字符串),然后event loop沒循環(huán)一次,在server cron中調用flushaofbuf,把aof_buf中的命令寫入aof文件(其實是write,真正寫入的是內核緩沖區(qū)),再清空aof_buf,進入下一次loop。這樣所有的數據庫的變化,都可以通過aof文件中的命令來還原,達到了保存數據庫的效果。
需要注意的是,flushaofbuf中調用的write,它只是把數據寫入了內核緩沖區(qū),真正寫入文件時內核自己決定的,可能需要延后一段時間。 不過redis支持配置,可以配置每次寫入后sync,則在redis里面調用sync,將內核中的數據寫入文件,這不過這要耗費一次系統(tǒng)調用,耗費時間而已。還可以配置策略為1秒鐘sync一次,則redis會開啟一個后臺線程(所以說redis不是單線程,只是單eventloop而已),這個后臺線程會每一秒調用一次sync。這里要問了,RDB的時候為什么沒有考慮sync的事情呢?因為RDB是一次性存儲的,不像AOF這樣多次存儲,RDB的時候調用一次sync也沒什么影響,而且使用bg save的時候,子進程會自己退出(exit),這時候exit函數內會沖刷緩沖區(qū),自動就寫入了文件中。
再來看,如果不想使用aof_buf保存每次的修改命令,也可以使用aof持久化。redis提供aof_rewrite,即根據現(xiàn)有的數據庫生成命令,然后把命令寫入aof文件中。很奇特吧?對,就是這么厲害。進行aof_rewrite的時候,redis變量每個數據庫,然后根據key-value對中value的具體類型,生成不同的命令,比如是list,則它生成一個保存list的命令,這個命令里包含了保存該list所需要的的數據,如果這個list數據過長,還會分成多條命令,先創(chuàng)建這個list,然后往list里面添加元素,總之,就是根據數據反向生成保存數據的命令。然后將這些命令存儲aof文件,這樣不就和aof append達到同樣的效果了么?
再來看,aof格式也支持后臺模式。執(zhí)行aof_bgrewrite的時候,也是fork一個子進程,然后讓子進程進行aof_rewrite,把它復制的數據庫寫入一個臨時文件,然后寫完后用新號通知父進程。父進程判斷子進程的退出信息是否正確,然后將臨時文件更名成最終的aof文件。好了,問題來了。在子進程持久化期間,可能父進程的數據庫有更新,怎么把這個更新通知子進程呢?難道要用進程間通信么?是不是有點麻煩呢?你猜redis怎么做的?它根本不通知子進程。什么,不通知?那更新怎么辦? 在子進程執(zhí)行aof_bgrewrite期間,父進程會保存所有對數據庫有更改的操作的命令(增,刪除,改等),把他們保存在aof_rewrite_buf_blocks中,這是一個鏈表,每個block都可以保存命令,存不下時,新申請block,然后放入鏈表后面即可,當子進程通知完成保存后,父進程將aof_rewrite_buf_blocks的命令append 進aof文件就可以了。多么優(yōu)美的設計,想一想自己當初還考慮用進程間通信,別人直接用最簡單的方法就完美的解決了問題,有句話說得真對,越優(yōu)秀的設計越趨于簡單,而復雜的東西往往都是靠不住的。
至于aof文件的載入,也就是一條一條的執(zhí)行aof文件里面的命令而已。不過考慮到這些命令就是客戶端發(fā)送給redis的命令,所以redis干脆生成了一個假的客戶端,它沒有和redis建立網絡連接,而是直接執(zhí)行命令即可。首先搞清楚,這里的假的客戶端,并不是真正的客戶端,而是存儲在redis里面的客戶端的信息,里面有寫和讀的緩沖區(qū),它是存在于redis服務器中的。所以,如下圖,直接讀入aof的命令,放入客戶端的讀緩沖區(qū)中,然后執(zhí)行這個客戶端的命令即可。這樣就完成了aof文件的載入。
// 創(chuàng)建偽客戶端 fakeClient = createFakeClient(); while(命令不為空) { // 獲取一條命令的參數信息 argc, argv ... // 執(zhí)行 fakeClient->argc = argc; fakeClient->argv = argv; cmd->proc(fakeClient); }
整個aof持久化的設計,個人認為相當精彩。其中有很多地方,值得膜拜。
5. redis的事務redis另一個比memcached強大的地方,是它支持簡單的事務。事務簡單說就是把幾個命令合并,一次性執(zhí)行全部命令。對于關系型數據庫來說,事務還有回滾機制,即事務命令要么全部執(zhí)行成功,只要有一條失敗就回滾,回到事務執(zhí)行前的狀態(tài)。redis不支持回滾,它的事務只保證命令依次被執(zhí)行,即使中間一條命令出錯也會繼續(xù)往下執(zhí)行,所以說它只支持簡單的事務。
首先看redis事務的執(zhí)行過程。首先執(zhí)行multi命令,表示開始事務,然后輸入需要執(zhí)行的命令,最后輸入exec執(zhí)行事務。 redis服務器收到multi命令后,會將對應的client的狀態(tài)設置為REDIS_MULTI,表示client處于事務階段,并在client的multiState結構體里面保持事務的命令具體信息(當然首先也會檢查命令是否能否識別,錯誤的命令不會保存),即命令的個數和具體的各個命令,當收到exec命令后,redis會順序執(zhí)行multiState里面保存的命令,然后保存每個命令的返回值,當有命令發(fā)生錯誤的時候,redis不會停止事務,而是保存錯誤信息,然后繼續(xù)往下執(zhí)行,當所有的命令都執(zhí)行完后,將所有命令的返回值一起返回給客戶。redis為什么不支持回滾呢?網上看到的解釋出現(xiàn)問題是由于客戶程序的問題,所以沒必要服務器回滾,同時,不支持回滾,redis服務器的運行高效很多。在我看來,redis的事務不是傳統(tǒng)關系型數據庫的事務,要求CIAD那么非常嚴格,或者說redis的事務都不是事務,只是提供了一種方式,使得客戶端可以一次性執(zhí)行多條命令而已,就把事務當做普通命令就行了,支持回滾也就沒必要了。
我們知道redis是單event loop的,在真正執(zhí)行一個事物的時候(即redis收到exec命令后),事物的執(zhí)行過程是不會被打斷的,所有命令都會在一個event loop中執(zhí)行完。但是在用戶逐個輸入事務的命令的時候,這期間,可能已經有別的客戶修改了事務里面用到的數據,這就可能產生問題。所以redis還提供了watch命令,用戶可以在輸入multi之前,執(zhí)行watch命令,指定需要觀察的數據,這樣如果在exec之前,有其他的客戶端修改了這些被watch的數據,則exec的時候,執(zhí)行到處理被修改的數據的命令的時候,會執(zhí)行失敗,提示數據已經dirty。 這是如何是實現(xiàn)的呢? 原來在每一個redisDb中還有一個dict watched_keys,watched_kesy中dictentry的key是被watch的數據庫的key,而value則是一個list,里面存儲的是watch它的client。同時,每個client也有一個watched_keys,里面保存的是這個client當前watch的key。在執(zhí)行watch的時候,redis在對應的數據庫的watched_keys中找到這個key(如果沒有,則新建一個dictentry),然后在它的客戶列表中加入這個client,同時,往這個client的watched_keys中加入這個key。當有客戶執(zhí)行一個命令修改數據的時候,redis首先在watched_keys中找這個key,如果發(fā)現(xiàn)有它,證明有client在watch它,則遍歷所有watch它的client,將這些client設置為REDIS_DIRTY_CAS,表面有watch的key被dirty了。當客戶執(zhí)行的事務的時候,首先會檢查是否被設置了REDIS_DIRTY_CAS,如果是,則表明數據dirty了,事務無法執(zhí)行,會立即返回錯誤,只有client沒有被設置REDIS_DIRTY_CAS的時候才能夠執(zhí)行事務。 需要指出的是,執(zhí)行exec后,該client的所有watch的key都會被清除,同時db中該key的client列表也會清除該client,即執(zhí)行exec后,該client不再watch任何key(即使exec沒有執(zhí)行成功也是一樣)。所以說redis的事務是簡單的事務,算不上真正的事務。
以上就是redis的事務,感覺實現(xiàn)很簡單,實際用處也不是太大。
6. redis的發(fā)布訂閱頻道redis支持頻道,即加入一個頻道的用戶相當于加入了一個群,客戶往頻道里面發(fā)的信息,頻道里的所有client都能收到。
實現(xiàn)也很簡單,也watch_keys實現(xiàn)差不多,redis server中保存了一個pubsub_channels的dict,里面的key是頻道的名稱(顯然要唯一了),value則是一個鏈表,保存加入了該頻道的client。同時,每個client都有一個pubsub_channels,保存了自己關注的頻道。當用用戶往頻道發(fā)消息的時候,首先在server中的pubsub_channels找到改頻道,然后遍歷client,給他們發(fā)消息。而訂閱,取消訂閱頻道不夠都是操作pubsub_channels而已,很好理解。
同時,redis還支持模式頻道。即通過正則匹配頻道,如有模式頻道p, 1, 則向普通頻道p1發(fā)送消息時,會匹配p,1,除了往普通頻道發(fā)消息外,還會往p,1模式頻道中的client發(fā)消息。注意,這里是用發(fā)布命令里面的普通頻道來匹配已有的模式頻道,而不是在發(fā)布命令里制定模式頻道,然后匹配redis里面保存的頻道。實現(xiàn)方式也很簡單,在redis server里面有個pubsub_patterns的list(這里為什么不用dict?因為pubsub_patterns的個數一般較少,不需要使用dict,簡單的list就好了),它里面存儲的是pubsubPattern結構體,里面是模式和client信息,如下所示,一個模式,一個client,所以如果有多個clint監(jiān)聽一個pubsub_patterns的話,在list面會有多個pubsubPattern,保存client和pubsub_patterns的對應關系。 同時,在client里面,也有一個pubsub_patterns list,不過里面存儲的就是它監(jiān)聽的pubsub_patterns的列表(就是sds),而不是pubsubPattern結構體。
typedef struct pubsubPattern { redisClient *client; // 監(jiān)聽的client robj *pattern; // 模式 } pubsubPattern;
當用戶往一個頻道發(fā)送消息的時候,首先會在redis server中的pubsub_channels里面查找該頻道,然后往它的客戶列表發(fā)送消息。然后在redis server里面的pubsub_patterns里面查找匹配的模式,然后往client里面發(fā)送消息。 這里并沒有去除重復的客戶,在pubsub_channels可能已經給某一個client發(fā)過message了,然后在pubsub_patterns中可能還會給用戶再發(fā)一次(甚至更多次)。 估計redis認為這是客戶程序自己的問題,所以不處理。
/* 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; }六. 總結
總的來看,redis比memcached的功能多很多,實現(xiàn)也更復雜。 不過memcached更專注于保存key-value數據(這已經能滿足大多數使用場景了),而redis提供更豐富的數據結構及其他的一些功能。不能說redis比memcached好,不過從源碼閱讀的角度來看,redis的價值或許更大一點。 另外,redis3.0里面支持了集群功能,這部分的代碼還沒有研究,后續(xù)再跟進。
問答
如何保持redis服務器運行
相關閱讀
基于hashicorp/raft的分布式一致性實戰(zhàn)教學
redis基本操作
Redis運維總結
【每日課程推薦】機器學習實戰(zhàn)!快速入門在線廣告業(yè)務及CTR相應知識
此文已由作者授權騰訊云+社區(qū)發(fā)布,更多原文請點擊
搜索關注公眾號「云加社區(qū)」,第一時間獲取技術干貨,關注后回復1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在云加社區(qū)!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/62029.html
摘要:所以查閱官方文檔以及他人造好的輪子,總結了一些面試和學習中你必須掌握的問題。在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合。 昨天寫了一篇自己搭建redis集群并在自己項目中使用的文章,今天早上看別人寫的面經發(fā)現(xiàn)redis在面試中還是比較常問的(筆主主Java方向)。所以查閱官方文檔以及他人造好的輪子,總結了一些redis面試和學習中你必須掌握的問題。...
摘要:單線程請求,所有命令串行執(zhí)行,并發(fā)情況下不需要考慮數據一致性問題。只能使用單線程,性能受限于性能,故單實例最高才可能達到取決于數據結構,數據大小以及服務器硬件性能,日常環(huán)境中高峰大約在左右。 1 基本概念 1.1 Redis(內存數據庫) Redis是一個key-value存儲系統(tǒng)(布式內緩存,高性能的key-value數據庫)。和Memcached類似,它支持存儲的value類型相對...
摘要:單線程請求,所有命令串行執(zhí)行,并發(fā)情況下不需要考慮數據一致性問題。只能使用單線程,性能受限于性能,故單實例最高才可能達到取決于數據結構,數據大小以及服務器硬件性能,日常環(huán)境中高峰大約在左右。 1 基本概念 1.1 Redis(內存數據庫) Redis是一個key-value存儲系統(tǒng)(布式內緩存,高性能的key-value數據庫)。和Memcached類似,它支持存儲的value類型相對...
閱讀 2488·2021-09-22 16:05
閱讀 2978·2021-09-10 11:24
閱讀 3647·2019-08-30 12:47
閱讀 2952·2019-08-29 15:42
閱讀 3394·2019-08-29 15:32
閱讀 1980·2019-08-26 11:48
閱讀 1096·2019-08-23 14:40
閱讀 908·2019-08-23 14:33