搜尋
首頁資料庫mysql教程Redis與Memcached有何不同 ? redis和Memcached的差異比較
Redis與Memcached有何不同 ? redis和Memcached的差異比較Sep 19, 2018 pm 03:50 PM
redis雲端運算程式設計師快取電腦科學

這篇文章帶給大家的內容是關於Redis與Memcached有何不同 ? redis和Memcached的差異比較,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

memcached和redis,作為近年來最常用的快取伺服器,相信大家對它們再熟悉不過了。前兩年還在學校時,我曾經讀過它們的主要源碼,如今寫篇筆記從個人角度簡單對比一下它們的實現方式,權當做複習,有理解錯誤之處,歡迎指正。

文中所使用的架構類別的圖片大多來自於網絡,有部分圖與最新實現有出入,文中已經指出。

一.綜述

讀一個軟體的源碼,首先要弄清楚軟體是用來做什麼的,那memcached和redis是乾啥的?眾所周知,資料一般會放在資料庫中,但是查詢資料會相對比較慢,特別是使用者很多時,頻繁的查詢,需要耗費大量的時間。怎麼辦呢?資料放在哪裡查詢快?那肯定是記憶體中。 memcached和redis就是將資料儲存在記憶體中,以key-value的方式查詢,可以大幅提高效率。所以一般它們都用做快取伺服器,快取常用的數據,需要查詢的時候,直接從它們那裡獲取,減少查詢資料庫的次數,提高查詢效率。

二. 服務方式

memcached和redis怎麼提供服務呢?它們是獨立的進程,需要的話,還可以讓他們變成daemon進程,所以我們的用戶進程要使用memcached和redis的服務的話,就需要進程間通信了。考慮到使用者進程和memcached和redis不一定在同一台機器上,所以還需要支援網路間通訊。因此,memcached和redis本身本身就是網頁伺服器,用戶進程透過與他們透過網路來傳輸數據,顯然最簡單、最常用的就是使用tcp連接了。另外,memcached和redis都支援udp協議。而且當使用者進程和memcached和redis在同一機器時,還可以使用unix域套接字通訊。

三.事件模型

下面開始講他們具體是怎麼實現的了。首先來看一下它們的事件模型。

自從epoll出來以後,幾乎所有的網頁伺服器全都拋棄select和poll,換成了epoll。 redis也一樣,只不多它還提供對select和poll的支持,可以自己配置使用哪一個,但是一般都是用epoll。另外針對BSD,也支援使用kqueue。而memcached是基於libevent的,不過libevent底層也是使用epoll的,所以可以認為它們都是在用epoll。 epoll的特性這裡就不介紹了,網路上介紹文章很多。

它們都使用epoll來做事件循環,不過redis是單線程的伺服器(redis也是多線程的,只不過除了主線程以外,其他線程沒有event loop,只是會進行一些後台存儲工作) ,而memcached是多線程的。 redis的事件模型很簡單,只有一個event loop,就是簡單的reactor實作。不過redis事件模型中有一個亮點,我們知道epoll是針對fd的,它回傳的就緒事件也是只有fd,redis裡面的fd就是伺服器與客戶端連接的socket的fd,但是處理的時候,需要根據這個fd找到具體的客戶端的訊息,怎麼找呢?通常的處理方式就是用紅黑樹將fd與客戶端資訊保存起來,透過fd查找,效率是lgn。不過redis比較特殊,redis的客戶端的數量上限可以設置,即可以知道同一時刻,redis所打開的fd的上限,而我們知道,進程的fd在同一時刻是不會重複的(fd只有關閉後才能復用),所以redis使用一個數組,將fd作為數組的下標,數組的元素就是客戶端的信息,這樣,直接透過fd就能定位客戶端信息,查找效率是O(1),還省去了複雜的紅黑樹的實作(我曾經用c寫一個網頁伺服器,就因為要保持fd和connect對應關係,不想自己寫紅黑樹,然後用了STL裡面的set,導致專案變成了c 的,最後專案使用g 編譯,這事我不說誰知道?)。顯然這種方式只能針對connection數量上限已確定,而且不是太大的網頁伺服器,像nginx這種http伺服器就不適用,nginx就是自己寫了紅黑樹。

而memcached是多線程的,使用master-worker的方式,主執行緒監聽端口,建立連接,然後順序分配給各個工作執行緒。每一個從線程都有一個event loop,它們服務不同的客戶端。 master線程和worker線程之間使用管道通信,每個工作線程都會建立一個管道,然後保存寫入和讀端,並且將讀端加入event loop,監聽可讀事件。同時,每個從線程都有一個就緒連接隊列,主線程連接連接後,將連接的item放入這個隊列,然後往該線程的管道的寫端寫入一個connect命令,這樣event loop中加入的管道讀端就會就緒,從線程讀取命令,解析命令發現是有連接,然後就會去自己的就緒隊列中獲取連接,並進行處理。多執行緒的優勢就是可以充分發揮多核心的優勢,不過寫程式麻煩一點,memcached裡面就有各種鎖和條件變數來進行執行緒同步。

四.記憶體分配

memcached和redis的核心任務都是在記憶體中操作數據,記憶體管理自然是核心的內容。

先看看他們的記憶體分配方式。 memcached是有自己得內存池的,即預先分配一大塊內存,然後接下來分配內存就從內存池中分配,這樣可以減少內存分配的次數,提高效率,這也是大部分網絡服務器的實現方式,只不過各個記憶體池的管理方式依具體情況而不同。而redis沒有自己得內存池,而是直接使用時分配,即什麼時候需要什麼時候分配,內存管理的事交給內核,自己只負責取和釋放(redis既是單線程,又沒有自己的內存池,是不是感覺實現的太簡單了?那是因為它的重點都放在資料庫模組了)。不過redis支援使用tcmalloc來取代glibc的malloc,前者是google的產品,比glibc的malloc快。

由於redis沒有自己的記憶體池,所以記憶體申請和釋放的管理就簡單很多,直接malloc和free即可,十分方便。而memcached是支援記憶體池的,所以記憶體申請是從記憶體池中獲取,而free也是還給記憶體池,所以需要很多額外的管理操作,實現起來麻煩很多,具體的會在後面memcached的slab機制講解中分析。

五.資料庫實作

接下來看看他們的最核心內容,各自資料庫的實作。

1. memcached資料庫實作

memcached只支援key-value,也就是只能一個key對一個value。它的資料在記憶體中也是這樣以key-value對的方式存儲,它使用slab機制。

首先看memcached是如何儲存資料的,也就是儲存key-value對。如下圖,每一個key-value對都儲存在一個item結構中,包含了相關的屬性和key和value的值。

Redis與Memcached有何不同 ? redis和Memcached的差異比較

item是儲存key-value對的,當item多的時候,怎麼找出特定的item是個問題。所以memcached維護了一個hash表,它用於快速查找item。 hash表適用開鏈法(與redis一樣)解決鍵的衝突,每一個hash表的桶裡面儲存了一個鍊錶,鍊錶節點就是item的指針,如上圖的h_next就是指桶裡面的鍊錶的下一個節點。 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,規則是比它大的最小的那個trunk。這樣,不同大小的item就分配在不同的slab中,歸不同的slabclass管理。這樣的缺點是會有部分記憶體浪費,因為一個trunk可能比item大,如圖2,分配100B的item的時候,選擇112的trunk,但是會有12B的浪費,這部分記憶體資源沒有使用。

Redis與Memcached有何不同 ? redis和Memcached的差異比較

Redis與Memcached有何不同 ? redis和Memcached的差異比較

Redis與Memcached有何不同 ? redis和Memcached的差異比較

#如上圖,整個構造就是這樣,slabclass管理slab,一個slabclass有一個slab_list,可以管理多個slab,同一個slabclass中的slab的trunk大小都一樣。 slabclass有一個指標slot,保存了未分配的item已經被free掉的item(不是真的free內存,只是不用了而已),有item不用的時候,就放入slot的頭部,這樣每次需要在目前slab中分配item的時候,直接取slot取即可,不用管item是未分配過的還是被釋放掉的。

然後,每一個slabclass對應一個鍊錶,有head數組和tail數組,它們分別保存了鍊錶的頭節點和尾節點。鍊錶中的節點就是改slabclass所分配的item,新分配的放在頭部,鍊錶越往後的item,表示它已經很久沒有被使用了。當slabclass的記憶體不足,需要刪除一些過期item的時候,就可以從鍊錶的尾端開始刪除,沒錯,這個鍊錶就是為了實作LRU。光靠它還不行,因為鍊錶的查詢是O(n)的,所以定位item的時候,使用hash表,這已經有了,所有分配的item已經在hash表中了,所以,hash用於查找item ,然後鍊錶有用儲存item的最近使用順序,這也是lru的標準實作方法。

每次需要新分配item的時候,找到slabclass對於的鍊錶,從尾部往前找,看item是否已經過期,過期的話,直接就用這個過期的item當做新的item。沒有過期的,則需要從slab中分配trunk,如果slab用完了,則需要往slabclass中加入slab了。

memcached支援設定過期時間,即expire time,但是內部並不定期檢查資料是否過期,而是客戶進程使用該資料的時候,memcached會檢查expire time,如果過期,直接回傳錯誤。這樣的優點是,不需要額外的cpu來進行expire time的檢查,缺點是有可能過期資料很久不被使用,則一直沒有被釋放,佔用記憶體。

memcached是多執行緒的,而且只維護了一個資料庫,所以可能有多個客戶進程操作同一個數據,這就有可能產生問題。例如,A已經把數據更改了,然後B也更改了改數據,那麼A的操作就被覆蓋了,而可能A不知道,A任務數據現在的狀態時他改完後的那個值,這樣就可能產生問題。為了解決這個問題,memcached使用了CAS協議,簡單說就是item保存一個64位的unsigned int值,標記數據的版本,每更新一次(數據值有修改),版本號增加,然後每次對數據進行更改操作,需要比對客戶進程傳來的版本號碼和伺服器這邊item的版本號是否一致,一致則可進行更改操作,否則提示髒數據。

以上就是memcached如何實作一個key-value的資料庫的介紹。

2. redis資料庫實作

首先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即可,高效方便。

為了實作這些資料結構,redis定義了抽象的物件redis object,如下圖。每一個物件有類型,總共5種:字串,鍊錶,集合,有序集合,哈希表。同時,為了提高效率,redis為每種類型準備了多種實作方式,根據特定的場景來選擇合適的實作方式,encoding就是表示物件的實作方式的。然後還有記錄了物件的lru,也就是上次被存取的時間,同時在redis 伺服器中會記錄一個目前的時間(近似值,因為這個時間只是每隔一定時間,伺服器進行自動維護的時候才更新),它們兩個只差就可以計算出物件多久沒有被造訪了。然後redis object中還有引用計數,這是為了共享對象,然後確定對象的刪除時間用的。最後使用一個void*指標來指向物件的真正內容。正式由於使用了抽象redis object,使得資料庫操作資料時方便很多,全部統一使用redis object物件即可,需要區分物件類型的時候,再根據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;        // 底部为了节省空间,一种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沒有現成的string,所以redis的首要任務就是實作一個string,取名叫sds(simple dynamic string),如下的程式碼, 非常簡單的一個結構體,len儲存改string的記憶體總長度,free表示還有多少位元組沒有使用,而buf儲存具體的數據,顯然len-free就是目前字串的長度。

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

字串解決了,所有的key都存成sds就行了,那麼key和value怎麼關聯呢? key-value的格式在腳本語言中很好處理,直接使用字典即可​​,C沒有字典,該怎麼辦呢?自己寫一個唄(redis十分熱衷於造輪子)。看下面的程式碼,privdata存額外資訊,用的很少,至少我們發現。 dictht是具體的哈希表,一個dict對應兩個哈希表,這是為了擴容(包括rehashidx也是為了擴容)。 dictType儲存了哈希表的屬性。 redis也為dict實作了迭代器(所以說看起來像c 程式碼)。

哈希表的具體實作是和mc類似的做法,也是使用開鏈法來解決衝突,不過裡面用到了一些小技巧。例如使用dictType儲存函數指針,可以動態配置桶裡面元素的操作方法。又例如dictht中保存的sizemask取size(桶的數量)-1,用它與key做&操作來代替取餘運算,加快速度等等。總的來看,dict裡面有兩個哈希表,每個哈希表的桶裡面儲存dictEntry鍊錶,dictEntry儲存特定的key和value。

前面說過,一個dict對於兩個dictht,是為了擴容(其實還有縮容)。正常的時候,dict只使用dictht[0],當dict[0]中已有entry的數量與桶的數量達到一定的比例後,就會觸發擴容和縮容操作,我們統稱為rehash,這時,為dictht[1]申請rehash後的大小的內存,然後把dictht[0]裡的數據往dictht[1]裡面移動,並用rehashidx記錄當前已經移動萬的桶的數量,當所有桶都移完後, rehash完成,這時將dictht[1]變成dictht[0], 將原來的dictht[0]變成dictht[1],變成null。不同於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,資料庫就好實現了。所有資料讀取儲存在dict中,key儲存成dictEntry中的key(string),用void* 指向一個redis object,它可以是5種類型中的任何一種。如下圖,結構構造是這樣,不過這個圖已經過時了,有一些與redis3.0不符合的地方。

Redis與Memcached有何不同 ? redis和Memcached的差異比較

5中type的對象,每一個都至少有兩種底層實作方式。 string有3種:REDIS_ENCODING_RAW, REDIS_ENCIDING_INT, REDIS_ENCODING_EMBSTR, list有:普通雙向鍊錶和壓縮鍊錶,壓縮鍊錶簡單的說,就是講數組改造成鍊錶,連續的空間,然後透過儲存的大小資訊來模擬鍊錶字串,相對普通鍊錶來說可以節省空間,不過有副作用,由於是連續的空間,所以改變內存大小的時候,需要重新分配,並且由於保存了字符串的字節大小,所有有可能引起連續更新(具體實現請詳細看代碼)。 set有dict和intset(全是整數的時候使用它來儲存), sorted set有:skiplist和ziplist, hashtable實作有壓縮列表和dict和ziplist。 skiplist就是跳表,它有接近紅黑樹的效率,但是實現起來比紅黑樹簡單很多,所以被採用(奇怪,這裡又不造輪子了,難道因為這個輪子有點難?)。 hash table可以使用dict實現,則改dict中,每個dictentry中key保存了key(這是哈希表中的鍵值對的key),而value則保存了value,它們都是string。而set中的dict,每個dictentry中key保存了set中具體的一個元素的值,value則為null。圖中的zset(有序集合)有誤,zset使用skiplist和ziplist實現,首先skiplist很好理解,就把它當做紅黑樹的替代品就行,和紅黑樹一樣,它也可以排序。怎麼用ziplist儲存zset呢?首先在zset中,每個set中的元素都有一個分數score,用它來排序。所以在ziplist中,依照分數大小,先存元素,再存它的score,再存下一個元素,然後score。這樣連續存儲,所以插入或刪除的時候,都需要重新分配記憶體。所以當元素超過一定數量,或是某個元素的字元數超過一定數量,redis就會選擇使用skiplist來實現zset(如果目前使用的是ziplist,會將這個ziplist中的資料取出,存入一個新的skiplist ,然後刪除改ziplist,這就是底層實作轉換,其餘類型的redis object也是可以轉換的)。另外,ziplist如何實現hashtable呢?其實也很簡單,就是儲存一個key,儲存一個value,再儲存一個key,再儲存一個value。還是順序存儲,與zset實作類似,所以當元素超過一定數量,或是某個元素的字元數超過一定數量時,就會轉換成hashtable來實作。各種底層實作方式是可以轉換的,redis可以根據情況選擇最合適的實作方式,這也是這樣使用類似物件導向的實作方式的好處。

要指出的是,使用skiplist來實作zset的時候,其實還用了一個dict,這個dict儲存一樣的鍵值對。為什麼呢?因為skiplist的查找只是lgn的(可能變成n),而dict可以到O(1), 所以使用一個dict來加速查找,由於skiplist和dict可以指向同一個redis object,所以不會浪費太多內存。另外使用ziplist實作zset的時候,為什麼不用dict來加速尋找呢?因為ziplist支援的元素個數很少(個數多時就轉換成skiplist了),順序遍歷也很快,所以不用dict了。

這樣看來,上面的dict,dictType,dictHt,dictEntry,redis object都是很考慮的,它們配合實現了一個具有物件導向色彩的靈活、高效資料庫。不得不說,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里面有个定时执行的函数,叫servercron,它是维护服务器的函数,在它里面,会对过期数据进行删除,注意不是全删,而是在一定的时间内,对每个数据库的expire dict里面的数据随机选取出来,如果过期,则删除,否则再选,直到规定的时间到。即随机选取过期的数据删除,这个操作的时间分两种,一种较长,一种较短,一般执行短时间的删除,每隔一定的时间,执行一次长时间的删除。这样可以有效的缓解光采用惰性删除而导致的内存浪费问题。

以上就是redis的数据的实现,与memcached不同,redis还支持数据持久化,这个下面介绍。

4.redis数据库持久化

redis和memcached的最大不同,就是redis支持数据持久化,这也是很多人选择使用redis而不是memcached的最大原因。 redis的持久化,分为两种策略,用户可以配置使用不同的策略。

4.1 RDB持久化 用户执行save或者bgsave的时候,就会触发RDB持久化操作。RDB持久化操作的核心思想就是把数据库原封不动的保存在文件里。

那如何存储呢?如下图, 首先存储一个REDIS字符串,起到验证的作用,表示是RDB文件,然后保存redis的版本信息,然后是具体的数据库,然后存储结束符EOF,最后用检验和。关键就是databases,看它的名字也知道,它存储了多个数据库,数据库按照编号顺序存储,0号数据库存储完了,才轮到1,然后是2, 一直到最后一个数据库。

Redis與Memcached有何不同 ? redis和Memcached的差異比較

每一个数据库存储方式如下,首先一个1字节的常量SELECTDB,表示切换db了,然后下一个接上数据库的编号,它的长度是可变的,然后接下来就是具体的key-value对的数据了。

Redis與Memcached有何不同 ? redis和Memcached的差異比較

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 <p>由上面的代码也可以看出,存储的时候,先检查expire time,如果已经过期,不存就行了,否则,则将expire time存下来,注意,及时是存储expire time,也是先存储它的类型为REDIS_RDB_OPCODE_EXPIRETIME_MS,然后再存储具体过期时间。接下来存储真正的key-value对,首先存储value的类型,然后存储key(它按照字符串存储),然后存储value,如下图。</p><p style="text-align: center;"><span class="img-wrap"><img src="/static/imghwm/default1.png" data-src="https://img.php.cn//upload/image/508/612/527/1537343252515813.jpg?x-oss-process=image/resize,p_40" class="lazy" title="1537343252515813.jpg" alt="Redis與Memcached有何不同 ? redis和Memcached的差異比較"></span></p><p>在rdbsaveobject中,会根据val的不同类型,按照不同的方式存储,不过从根本上来看,最终都是转换成字符串存储,比如val是一个linklist,那么先存储整个list的字节数,然后遍历这个list,把数据取出来,依次按照string写入文件。对于hash table,也是先计算字节数,然后依次取出hash table中的dictEntry,按照string的方式存储它的key和value,然后存储下一个dictEntry。 总之,RDB的存储方式,对一个key-value对,会先存储expire time(如果有的话),然后是value的类型,然后存储key(字符串方式),然后根据value的类型和底层实现方式,将value转换成字符串存储。这里面为了实现数据压缩,以及能够根据文件恢复数据,redis使用了很多编码的技巧,有些我也没太看懂,不过关键还是要理解思想,不要在意这些细节。</p><p>保存了RDB文件,当redis再启动的时候,就根据RDB文件来恢复数据库。由于以及在RDB文件中保存了数据库的号码,以及它包含的key-value对,以及每个key-value对中value的具体类型,实现方式,和数据,redis只要顺序读取文件,然后恢复object即可。由于保存了expire time,发现当前的时间已经比expire time大了,即数据已经超时了,则不恢复这个key-value对即可。</p><p>保存RDB檔案是一個很巨大的工程,所以redis也提供後台保存的機制。也就是執行bgsave的時候,redis fork出一個子程序,讓子程序來執行已儲存的工作,而父行程繼續提供redis正常的資料庫服務。由於子程序複製了父程序的位址空間,即子程序擁有父程序fork時的資料庫,子程序執行save的操作,把它從父程序那裡繼承來的資料庫寫入一個temp檔即可。在子行程複製期間,redis會記錄資料庫的修改次數(dirty)。當子進程完成時,發送給父進程SIGUSR1訊號,父進程捕捉到這個訊號,就知道子進程完成了複製,然後父進程將子進程保存的temp檔改名為真正的rdb檔(即真正保存成功了才改成目標文件,這才是保險的做法)。然後記錄下這次save的結束時間。 </p><p>這裡有一個問題,在子進程保存期間,父進程的資料庫已經被修改了,而父進程只是記錄了修改的次數(dirty),被沒有進行修正操作。似乎使得RDB保存的不是即時的資料庫,有點不那麼高大上的樣子。不過後面要介紹的AOF持久化,就解決了這個問題。 </p><p>除了客戶執行sava或bgsave指令,還可以設定RDB儲存條件。即在設定檔中配置,在t時間內,資料庫被修改了dirty次,則進行後台儲存。 redis在serve cron的時候,會根據dirty數目和上次保存的時間,來判斷是否符合條件,符合條件的話,就進行bg save,注意,任意時刻只能有一個子進程來進行後台保存,因為保存是個很費io的操作,多個進程大量io效率不行,而且不好管理。 </p><p><strong>4.2 AOF持久化</strong> 先想一個問題,儲存資料庫一定需要像RDB一樣把資料庫裡面的所有資料保存下來麼?有沒有別的方法? </p><p>RDB保存的只是最終的資料庫,它是一個結果。結果是怎麼來的?是透過使用者的各個命令建立起來的,所以可以不保存結果,而只保存建立這個結果的命令。 redis的AOF就是這個思想,它不同RDB保存db的數據,它保存的是一條一條建立資料庫的指令。 </p><p>我們先來看AOF檔的格式,它裡面保存的是一條一條的指令,先儲存指令長度,然後儲存指令,具體的分隔符號什麼的可以自己深入研究,這都不是重點,反正知道AOF檔案儲存的是redis客戶端執行的指令即可。 </p><p>redis server中有一個sds aof_buf, 如果aof持久化開啟的話,每個修改資料庫的指令都會存入這個aof_buf(儲存的是aof檔案中指令格式的字串),然後event loop沒迴圈一次,在server cron中呼叫flushaofbuf,把aof_buf中的指令寫入aof檔(其實是write,真正寫入的是核心緩衝區),再清空aof_buf,進入下次loop。這樣所有的資料庫的變化,都可以透過aof檔案中的指令來還原,達到了保存資料庫的效果。 </p><p>要注意的是,flushaofbuf中呼叫的write,它只是把資料寫入了核心緩衝區,真正寫入檔案時核心自己決定的,可能需要延後一段時間。不過redis支援配置,可以配置每次寫入後sync,則在redis裡面調用sync,將內核中的資料寫入文件,這不過這要耗費一次系統調用,耗費時間而已。還可以配置策略為1秒鐘sync一次,則redis會開啟一個後台線程(所以說redis不是單線程,只是單eventloop而已),這個後台線程會每一秒調用一次sync。這裡要問了,RDB的時候為什麼沒有考慮到sync的事情呢?因為RDB是一次性儲存的,不像AOF這樣多次存儲,RDB的時候調用一次sync也沒什麼影響,而且使用bg save的時候,子進程會自己退出(exit),這時候exit函數內會沖刷緩衝區,自動就寫入了文件中。 </p><p>再來看,如果不想使用aof_buf保存每次的修改指令,也可以使用aof持久化。 redis提供aof_rewrite,也就是根據現有的資料庫產生指令,然後把指令寫入aof檔案。很奇特吧?對,就是這麼厲害。進行aof_rewrite的時候,redis變數每個資料庫,然後根據key-value對中value的具體類型,產生不同的命令,例如是list,則它產生一個保存list的命令,這個命令裡包含了保存該list所所需的數據,如果這個list數據過長,還會分成多個指令,先創建這個list,然後往list裡面加入元素,總之,就是根據數據反向生成保存數據的命令。然後將這些指令儲存aof文件,這樣不就和aof append達到同樣的效果了麼? </p><p>再来看,aof格式也支持后台模式。执行aof_bgrewrite的时候,也是fork一个子进程,然后让子进程进行aof_rewrite,把它复制的数据库写入一个临时文件,然后写完后用新号通知父进程。父进程判断子进程的退出信息是否正确,然后将临时文件更名成最终的aof文件。好了,问题来了。在子进程持久化期间,可能父进程的数据库有更新,怎么把这个更新通知子进程呢?难道要用进程间通信么?是不是有点麻烦呢?你猜redis怎么做的?它根本不通知子进程。什么,不通知?那更新怎么办? 在子进程执行aof_bgrewrite期间,父进程会保存所有对数据库有更改的操作的命令(增,删除,改等),把他们保存在aof_rewrite_buf_blocks中,这是一个链表,每个block都可以保存命令,存不下时,新申请block,然后放入链表后面即可,当子进程通知完成保存后,父进程将aof_rewrite_buf_blocks的命令append 进aof文件就可以了。多么优美的设计,想一想自己当初还考虑用进程间通信,别人直接用最简单的方法就完美的解决了问题,有句话说得真对,越优秀的设计越趋于简单,而复杂的东西往往都是靠不住的。</p><p>至于aof文件的载入,也就是一条一条的执行aof文件里面的命令而已。不过考虑到这些命令就是客户端发送给redis的命令,所以redis干脆生成了一个假的客户端,它没有和redis建立网络连接,而是直接执行命令即可。首先搞清楚,这里的假的客户端,并不是真正的客户端,而是存储在redis里面的客户端的信息,里面有写和读的缓冲区,它是存在于redis服务器中的。所以,如下图,直接读入aof的命令,放入客户端的读缓冲区中,然后执行这个客户端的命令即可。这样就完成了aof文件的载入。</p><pre class="brush:php;toolbar:false">// 创建伪客户端
fakeClient = createFakeClient();

while(命令不为空) {
   // 获取一条命令的参数信息 argc, argv
   ...

    // 执行
    fakeClient->argc = argc;
    fakeClient->argv = argv;
    cmd->proc(fakeClient);
}

整个aof持久化的设计,个人认为相当精彩。其中有很多地方,值得膜拜。

5. redis的事务

redis另一个比memcached强大的地方,是它支持简单的事务。事务简单说就是把几个命令合并,一次性执行全部命令。对于关系型数据库来说,事务还有回滚机制,即事务命令要么全部执行成功,只要有一条失败就回滚,回到事务执行前的状态。redis不支持回滚,它的事务只保证命令依次被执行,即使中间一条命令出错也会继续往下执行,所以说它只支持简单的事务。

首先看redis事务的执行过程。首先执行multi命令,表示开始事务,然后输入需要执行的命令,最后输入exec执行事务。 redis服务器收到multi命令后,会将对应的client的状态设置为REDIS_MULTI,表示client处于事务阶段,并在client的multiState结构体里面保持事务的命令具体信息(当然首先也会检查命令是否能否识别,错误的命令不会保存),即命令的个数和具体的各个命令,当收到exec命令后,redis会顺序执行multiState里面保存的命令,然后保存每个命令的返回值,当有命令发生错误的时候,redis不会停止事务,而是保存错误信息,然后继续往下执行,当所有的命令都执行完后,将所有命令的返回值一起返回给客户。redis为什么不支持回滚呢?网上看到的解释出现问题是由于客户程序的问题,所以没必要服务器回滚,同时,不支持回滚,redis服务器的运行高效很多。在我看来,redis的事务不是传统关系型数据库的事务,要求CIAD那么非常严格,或者说redis的事务都不是事务,只是提供了一种方式,使得客户端可以一次性执行多条命令而已,就把事务当做普通命令就行了,支持回滚也就没必要了。

Redis與Memcached有何不同 ? redis和Memcached的差異比較

我们知道redis是单event loop的,在真正执行一个事物的时候(即redis收到exec命令后),事物的执行过程是不会被打断的,所有命令都会在一个event loop中执行完。但是在用户逐个输入事务的命令的时候,这期间,可能已经有别的客户修改了事务里面用到的数据,这就可能产生问题。所以redis还提供了watch命令,用户可以在输入multi之前,执行watch命令,指定需要观察的数据,这样如果在exec之前,有其他的客户端修改了这些被watch的数据,则exec的时候,执行到处理被修改的数据的命令的时候,会执行失败,提示数据已经dirty。 这是如何是实现的呢? 原来在每一个redisDb中还有一个dict watched_keys,watched_kesy中dictentry的key是被watch的数据库的key,而value则是一个list,里面存储的是watch它的client。同时,每个client也有一个watched_keys,里面保存的是这个client当前watch的key。在执行watch的时候,redis在对应的数据库的watched_keys中找到这个key(如果没有,则新建一个dictentry),然后在它的客户列表中加入这个client,同时,往这个client的watched_keys中加入这个key。当有客户执行一个命令修改数据的时候,redis首先在watched_keys中找这个key,如果发现有它,证明有client在watch它,则遍历所有watch它的client,将这些client设置为REDIS_DIRTY_CAS,表面有watch的key被dirty了。当客户执行的事务的时候,首先会检查是否被设置了REDIS_DIRTY_CAS,如果是,则表明数据dirty了,事务无法执行,会立即返回错误,只有client没有被设置REDIS_DIRTY_CAS的时候才能够执行事务。 需要指出的是,执行exec后,该client的所有watch的key都会被清除,同时db中该key的client列表也会清除该client,即执行exec后,该client不再watch任何key(即使exec没有执行成功也是一样)。所以说redis的事务是简单的事务,算不上真正的事务。

以上就是redis的事务,感觉实现很简单,实际用处也不是太大。

6. redis的发布订阅频道

redis支持频道,即加入一个频道的用户相当于加入了一个群,客户往频道里面发的信息,频道里的所有client都能收到。

实现也很简单,也watch_keys实现差不多,redis server中保存了一个pubsub_channels的dict,里面的key是频道的名称(显然要唯一了),value则是一个链表,保存加入了该频道的client。同时,每个client都有一个pubsub_channels,保存了自己关注的频道。当用用户往频道发消息的时候,首先在server中的pubsub_channels找到改频道,然后遍历client,给他们发消息。而订阅,取消订阅频道不够都是操作pubsub_channels而已,很好理解。

同时,redis还支持模式频道。即通过正则匹配频道,如有模式频道p, 1, 则向普通频道p1发送消息时,会匹配p1,除了往普通频道发消息外,还会往p1模式频道中的client发消息。注意,这里是用发布命令里面的普通频道来匹配已有的模式频道,而不是在发布命令里制定模式频道,然后匹配redis里面保存的频道。实现方式也很简单,在redis server里面有个pubsub_patterns的list(这里为什么不用dict?因为pubsub_patterns的个数一般较少,不需要使用dict,简单的list就好了),它里面存储的是pubsubPattern结构体,里面是模式和client信息,如下所示,一个模式,一个client,所以如果有多个clint监听一个pubsub_patterns的话,在list面会有多个pubsubPattern,保存client和pubsub_patterns的对应关系。 同时,在client里面,也有一个pubsub_patterns list,不过里面存储的就是它监听的pubsub_patterns的列表(就是sds),而不是pubsubPattern结构体。

typedef struct pubsubPattern {
    redisClient *client;    // 监听的client
    robj *pattern;            // 模式
} pubsubPattern;

当用户往一个频道发送消息的时候,首先会在redis server中的pubsub_channels里面查找该频道,然后往它的客户列表发送消息。然后在redis server里面的pubsub_patterns里面查找匹配的模式,然后往client里面发送消息。 这里并没有去除重复的客户,在pubsub_channels可能已经给某一个client发过message了,然后在pubsub_patterns中可能还会给用户再发一次(甚至更多次)。 估计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的功能多很多,实现也更复杂。 不过memcached更专注于保存key-value数据(这已经能满足大多数使用场景了),而redis提供更丰富的数据结构及其他的一些功能。不能说redis比memcached好,不过从源码阅读的角度来看,redis的价值或许更大一点。 另外,redis3.0里面支持了集群功能,这部分的代码还没有研究,后续再跟进。

以上是Redis與Memcached有何不同 ? redis和Memcached的差異比較的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
云计算与web前端挂钩吗云计算与web前端挂钩吗Jan 29, 2023 am 10:45 AM

云计算与web前端有挂钩。云计算在web前端的体现就是可以到云里拿一些资源来支撑业务;这些资源可以是计算能力、存储空间等硬件资源,也可以是各种应用、服务甚至桌面等软件资源。再次细分之后可以看到,当云计算体现到前端时,终端用户获得的要么是应用,要么是桌面;那桌面云的概念就应运而生了。桌面云的重点也在于应用,为用户搭建了种种桌面云应用环境,解决用户所遇到的各种业务问题。

人工智能能取代云计算架构师吗?人工智能能取代云计算架构师吗?Apr 07, 2023 pm 10:27 PM

人工智能系统每天都令人印象深刻。如今的人工智能可以自动化许多信息工作者的任务,因此那些从事云计算工作的人担心自己会成为下一个。人们对人工智能及其应用的兴趣大约在五年前发生了变化。后来发生了大流行,一些预算转向了快速云迁移。现在一切都恢复正常了,人工智能又回来了。大多数企业都掌握了人工智能的基本可能性,并希望将这项技术武器化,用于自己的业务。在这个过程中,技术变得更加令人印象深刻。例如,随着ChatGPT等生成式AI服务的出现,生成式人工智能从博士论文变成了可访问的免费现实。生成式人工智能是一种基

人工智能在云计算中能起到怎样的作用?人工智能在云计算中能起到怎样的作用?Apr 09, 2023 am 11:31 AM

在当今的数字世界中,人工智能和云计算每天影响着许多人的工作和生活。云计算帮助企业变得更加敏捷和灵活,并提供成本效益。借助人工智能技术,有助于从数据中产生洞察力,提供卓越的客户体验。因此,协同人工智能和云计算解决方案将使企业更接近其最终客户并提高其运营效率。云计算及人工智能是什么?云计算是分布式计算的一种,指的是通过网络“云”将巨大的数据计算处理程序分解成无数个小程序,然后,通过多部服务器组成的系统进行处理和分析这些小程序得到结果并返回给用户。云计算基于按需付费的定价原则。简单来说,云计算可以定义

在人工智能基础设施方面,您应该选择边缘或云?在人工智能基础设施方面,您应该选择边缘或云?May 08, 2023 pm 11:58 PM

​如今,边缘计算一直是热门话题。被誉为近年来最令人兴奋的技术转变,关于其变革力量的讨论很多!随着越来越强大的AI/ML算法重新定义“智能”以及更便宜、更强大的“边缘”设备的可用性,这种炒作在很大程度上是真实的。但是,如果要考虑边缘计算的历史,它会比最近的兴趣让我们相信的更早。事实上,计算和智能最初始于边缘,当时大多数应用程序几乎不存在高带宽网络连接。即使在1990年代后期,远程部署在工厂或现场的关键测量设备通常也具有处理传入传感器数据的专用计算能力。然而,这些设备中的算法在“智能”方面只是初级的

2022 年最佳 Windows 11 备份软件,推荐 3+ 款工具2022 年最佳 Windows 11 备份软件,推荐 3+ 款工具Apr 26, 2023 pm 11:22 PM

无论它多么先进,技术都可能会发生令人讨厌的转变,您可能离丢失文件只有一步之遥。例如,硬盘驱动器因崩溃而臭名昭著,而如今的勒索软件可以使计算机的内容无法访问。随着数字内容对企业以及包括视频、照片和音乐在内的个人资产变得至关重要,很明显,使用备份软件保护一切变得比以往任何时候都更加重要。为什么要使用备份软件?您需要使用备份软件的原因有很多。您的设备可能被盗,您的硬盘驱动器可能会意外崩溃,或者您可能成为恶意软件的受害者。如果您不想冒丢失所有数据的风险,请考虑使用备份软件,最好是基于云的软件,因为它们比

云计算通用可编程DPU发展云计算通用可编程DPU发展Apr 07, 2023 pm 07:31 PM

着眼于数据中心的技术发展和演进,DPU 作为通用的数据处理器,它不是对 NIC/SmartNIC 的简单替代,而是对网络基础架构的本质改变。DPU 本身所具有的通用层级化可编程性、低时延网络、统一管控的特性使得 DPU 正在促进新一代数据中心从架构上实现优化和重构。DPU 作为通用数据处理的基础部件,将卸载原本运行在CPU、GPU中的通用数据处理任务,释放CPU、GPU 的算力,支撑 CPU、GPU 发挥更大的效能。“云计算通用可编程DPU发展白皮书(2023年)”白皮书通过阐明和分析 DPU

云计算架构分为哪三个层次云计算架构分为哪三个层次Dec 23, 2020 pm 04:19 PM

云计算架构分为基础设施层、平台层和软件服务层三个层次,云计算的目的是通过基于网络的计算方式,将共享的软件或硬件资源和信息进行组织整合,按需提供给计算机或其他系统使用。

Microsoft Security 通过多云功能升级用户防护能力Microsoft Security 通过多云功能升级用户防护能力Apr 24, 2023 pm 09:07 PM

微软详细介绍了它计划如何通过将多云支持引入其主要安全策略来进一步保护客户的安全。与此相一致,该公司旨在引入新功能和进步,以帮助从集中管理的角度加强安全性和控制。为了尝试向其客户引入更简单的安全解决方案,以便他们不会发现管理多云环境具有挑战性,微软决定将“MicrosoftDefenderforCloud的本机功能扩展到谷歌云平台(GCP)”。得益于GCP的支持,微软成为唯一一家为业界前三大平台提供原生多云保护的云提供商,这些平台分别是:微软Azure、亚马逊网络服务(AWS)

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
2 週前By尊渡假赌尊渡假赌尊渡假赌
倉庫:如何復興隊友
4 週前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒險:如何獲得巨型種子
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境