自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

五分鐘讓你了解Redis和Memecache的區(qū)別

數據庫 Redis
Redis 支持頻道功能,允許用戶創(chuàng)建頻道并加入其中,形成一個類似于消息群組的機制。在頻道中,任何用戶發(fā)送的消息都會被頻道內的所有訂閱者接收到。

綜述

Memcached和Redis都是高性能的內存數據存儲系統(tǒng),通常用作緩存服務器。它們以key-value的形式存儲數據,使得數據的訪問速度非???。當應用程序需要頻繁地讀取或寫入數據時,如果每次都從數據庫中進行操作,不僅會造成數據庫的壓力增大,而且查詢效率也會降低。因此,我們可以將一部分常用的、熱點數據存儲在Memcached或Redis這樣的內存數據存儲系統(tǒng)中。

當用戶發(fā)起查詢請求時,應用程序首先會檢查Memcached或Redis中是否存在所需的數據。如果存在,則直接從內存中獲取數據并返回給用戶,避免了對數據庫的查詢,從而大大提高了查詢效率。如果Memcached或Redis中沒有所需的數據,則應用程序再從數據庫中查詢,并將查詢到的數據存入Memcached或Redis中,以備下次使用。

服務方式

Memcached和Redis作為獨立的服務進程運行,它們通過監(jiān)聽特定的網絡端口或Unix域套接字來提供服務。這些服務進程可以配置為在系統(tǒng)啟動時自動作為守護進程(daemon)運行,確保服務的持續(xù)可用性。

當其他用戶進程需要使用Memcached或Redis的服務時,它們會通過網絡或Unix域套接字與這些服務進程進行通信。由于用戶進程和Memcached/Redis服務可能部署在不同的機器上,因此網絡通信是必需的。而TCP是最常用且最可靠的通信協(xié)議之一,因此Memcached和Redis都支持TCP連接。

除了TCP,Memcached還支持UDP協(xié)議進行通信,盡管UDP不提供像TCP那樣的可靠連接和錯誤恢復機制,但它在某些場景下可能提供更快的性能。Redis也支持UDP,但在實際應用中較少使用,因為Redis更側重于數據的一致性和可靠性。

另外,當Memcached或Redis服務與用戶進程位于同一臺機器上時,為了提高性能并減少網絡開銷,它們還支持使用Unix域套接字(Unix Domain Socket)進行通信。Unix域套接字是一種在同一臺機器上的進程間通信機制,它使用文件系統(tǒng)路徑名作為地址,而不是像TCP和UDP那樣使用網絡地址和端口號。

事件模型

自從epoll(一種Linux特有的I/O事件通知機制)問世以來,由于其高效的性能和可擴展性,它幾乎成了網絡服務器領域的首選I/O多路復用技術。因此,大多數現(xiàn)代的網絡服務器,包括Redis,已經放棄了傳統(tǒng)的select和poll機制,轉而采用epoll。Redis雖然仍然保留了對select和poll的支持,但在實際部署中,用戶通常都會選擇使用epoll以獲取更好的性能。

對于基于BSD的系統(tǒng),服務器軟件則傾向于使用kqueue,它是BSD系統(tǒng)提供的一種與epoll類似的I/O事件通知機制。

至于memcached,雖然它基于libevent庫進行網絡I/O處理,但libevent庫在內部實現(xiàn)時,對于Linux平臺也是優(yōu)先使用epoll作為其后端機制。因此,可以認為在使用Linux作為操作系統(tǒng)的環(huán)境中,無論是直接使用epoll還是通過libevent這樣的庫間接使用,最終的效果都是利用了epoll的高效性能。

圖片圖片

Memcached和Redis都利用epoll機制來處理事件循環(huán),但它們在線程使用上有所不同。Redis主要是單線程模型,其核心的事件處理是由單一線程完成的。這意味著Redis的主要操作,如接收請求、處理數據、返回結果,都在同一個線程中串行執(zhí)行。雖然Redis也有多線程的應用場景,但這些線程通常用于執(zhí)行像數據持久化這樣的后臺任務,并不參與事件循環(huán)。

而Memcached則采用了多線程模型,它能夠利用多個線程并行處理來自不同客戶端的請求,從而提高了整體的并發(fā)處理能力。

在Redis的事件模型中,雖然epoll機制能夠監(jiān)控文件描述符(fd)的就緒狀態(tài),但僅僅知道哪個fd就緒是不夠的。為了找到與這個fd關聯(lián)的客戶端信息,Redis使用了一種高效的數據結構——紅黑樹。它將每個fd作為鍵,將對應的客戶端信息作為值存儲在樹中。當epoll通知Redis有fd就緒時,Redis便可以在紅黑樹中快速查找到與該fd關聯(lián)的客戶端信息,進而執(zhí)行相應的操作。這種設計使得Redis能夠高效地處理大量的并發(fā)連接。

與一些其他系統(tǒng)不同,Redis允許你設置客戶端連接的數量上限,這意味著它可以預知在任何給定時間打開的文件描述符(fd)的最大數量。這一特性使得Redis能夠以一種非常高效的方式來管理客戶端連接。

由于文件描述符在同一時間內是唯一的,Redis利用了這一特性,采用了一個簡單而直接的方法來處理連接信息。它使用一個數組,其中文件描述符(fd)直接作為數組的索引。這樣,當Redis需要查找與特定文件描述符相關聯(lián)的客戶端信息時,它只需簡單地使用該文件描述符作為數組的索引,從而直接訪問到相應的客戶端信息。這種方法的查找效率達到了O(1),因為它避免了復雜的搜索算法或數據結構的開銷。

然而,這種方法的適用性是有局限性的。它主要適用于那些連接數量有限且相對固定的網絡服務器。對于像Nginx這樣的HTTP服務器,由于其需要處理大量且不斷變化的連接,因此使用更復雜的數據結構(如紅黑樹)來管理連接信息可能更為合適。這些數據結構能夠更有效地處理大量的數據插入、刪除和查找操作,確保在高并發(fā)環(huán)境下依然保持高效的性能。

而Memcached是一個多線程的內存數據存儲系統(tǒng),它采用了master-worker的架構。在這種架構中,主線程(master thread)負責監(jiān)聽端口和建立連接,一旦有新的連接請求,主線程會將這些連接順序分配給各個工作線程(worker threads)。

每個工作線程都有自己獨立的事件循環(huán)(event loop),它們各自服務不同的客戶端請求。為了與主線程進行通信,每個工作線程都會創(chuàng)建一個管道(pipe),并保存其寫端和讀端。然后,工作線程將管道的讀端加入其事件循環(huán)中,以便監(jiān)聽可讀事件。

當主線程建立一個新的連接時,它會將連接項放入對應工作線程的就緒連接隊列中。接著,主線程會向該工作線程的管道寫端寫入一個連接命令。這樣,工作線程的事件循環(huán)中監(jiān)控的管道讀端就會觸發(fā)就緒事件。工作線程會讀取這個命令,解析后發(fā)現(xiàn)是一個連接請求,于是它會從自己的就緒連接隊列中取出這個連接,并進行相應的處理。

多線程的好處在于能夠充分利用多核處理器的性能優(yōu)勢,提高系統(tǒng)的整體吞吐量。然而,多線程編程也會帶來一些復雜性,比如需要處理線程間的同步和通信問題。在Memcached中,為了避免數據競爭和不一致,使用了各種鎖和條件變量來進行線程同步。

內存分配

memcached和redis的核心任務都是在內存中操作數據,所以內存管理才是最核心的內容。

Memcached和Redis在內存分配方式上有所不同。Memcached采用的是內存池策略,這意味著它預先分配一大塊內存,并在后續(xù)的內存分配請求中從這塊內存池中滿足。這種做法減少了內存分配的次數,從而提高了效率。許多網絡服務器也采用類似的策略,但每個內存池的具體管理方式可能因應用場景而異。

相比之下,Redis并沒有采用內存池方式。它采取的是即時分配策略,即當需要內存時直接進行分配,并將內存管理的任務交給操作系統(tǒng)內核處理。Redis只負責內存的申請和釋放操作。雖然Redis是單線程的,并且沒有自己的內存池,但其設計使得內存管理變得相對簡單。Redis還提供了使用tcmalloc替換glibc的malloc的選項,這是Google開發(fā)的一款內存分配器,它在某些情況下比glibc的malloc更為高效。

由于Redis沒有自己的內存池,其內存申請和釋放操作變得十分直接和方便,只需使用標準的malloc和free函數即可。而Memcached的內存管理則相對復雜,因為它需要維護自己的內存池,并在分配和釋放內存時進行相應的管理操作。具體的實現(xiàn)細節(jié)和復雜性將在后續(xù)關于Memcached的slab機制的講解中進一步分析。

數據庫實現(xiàn)

Memcached數據庫實現(xiàn)

Memcached是一個高性能的分布式內存對象緩存系統(tǒng),它主要支持簡單的key-value數據存儲模型。在Memcached中,每個數據項都以key-value對的形式存在,其中key是唯一的標識符,而value是與該key相關聯(lián)的數據。這種存儲方式使得數據的存取變得非常高效。

為了管理這些key-value對,Memcached采用了稱為“slab”的內存管理機制。Slab分配器負責內存的分配和回收,它將內存劃分為不同大小的塊(或稱為slab class),每個塊的大小是固定的。當需要存儲一個key-value對時,Memcached會根據value的大小選擇一個合適的slab塊來存儲它。這種方式可以減少內存碎片,提高內存利用率

圖片圖片

Memcached利用哈希表來高效地存儲和查找大量的key-value對(即items)。為了快速定位特定的item,Memcached維護了一個精心設計的哈希表。當item數量增多時,這個哈希表能夠支持快速的查找操作。

哈希表采用了開鏈法(也稱為鏈地址法)來處理鍵的沖突。這意味著每個哈希表的桶(bucket)內部都維護了一個鏈表,鏈表中的節(jié)點是指向item的指針。這種結構允許多個具有相同哈希值的鍵(即沖突的鍵)能夠鏈接在一起。

隨著item數量的增長,哈希表可能需要擴容以維持高效的查找性能。當item數量達到桶數量的1.5倍以上時,Memcached會觸發(fā)擴容操作。擴容過程中,會先將舊的哈希表(old_hashtable)設置為當前使用的哈希表(primary_hashtable),然后為primary_hashtable分配一個新的、桶數量翻倍的哈希表。接著,系統(tǒng)會逐個將old_hashtable中的數據遷移到新的primary_hashtable中。這個過程通過一個名為expand_bucket的變量來跟蹤已經遷移了多少個桶。一旦數據遷移完成,舊的old_hashtable`就會被釋放,從而完成擴容。

值得一提的是,Memcached的擴容操作是由一個專門的后臺線程來完成的。當需要擴容時,系統(tǒng)會通過條件變量通知這個后臺線程。擴容完成后,后臺線程會再次阻塞,等待下一個擴容條件變量的觸發(fā)。這種設計允許擴容操作在不影響正常查找操作的情況下進行,提高了系統(tǒng)的整體性能。

在擴容過程中,查找一個item可能需要同時檢查primary_hashtable和old_hashtable。這取決于item的桶位置與expand_bucket的大小關系。這種雙表查找策略確保了即使在擴容過程中,系統(tǒng)也能準確地定位到每個item。

在Memcached中,item的內存分配是通過slab機制來實現(xiàn)的。這個機制包含多個slab class,每個slab class負責管理一組稱為trunks的內存塊。每個trunk的大小是固定的,而不同的slab class之間的trunk大小則按照一定的比例遞增。這種設計使得Memcached可以根據item的大小來靈活分配內存。

當一個新的item需要被創(chuàng)建時,Memcached會根據item的大小選擇一個合適的slab class。選擇的規(guī)則是找到比item大小稍大的最小trunk。例如,如果一個item的大小是100字節(jié),Memcached會選擇一個大小為112字節(jié)的trunk來分配內存。雖然這樣做會導致一些內存浪費(在這個例子中,有12字節(jié)的內存沒有被使用),但它也帶來了性能上的優(yōu)勢。

通過預先分配和管理內存塊(即trunks),Memcached能夠減少頻繁的內存分配和釋放操作,從而提高內存使用的效率和性能。這種內存管理方式雖然會有一定的內存浪費,但在許多情況下,這種浪費是可以接受的,因為它換取了更好的性能和可伸縮性。

圖片圖片

圖片圖片

圖片圖片

如上圖,整個構造就是這樣;Memcached實現(xiàn)了一個高效的key-value數據庫,其數據存儲和管理機制獨特而精巧。在Memcached中,數據以key-value對的形式存在,每個這樣的對都被封裝在一個item結構中,包含了key、value以及相關屬性。

這些item被組織成多個slab,而每個slab則由相應的slabclass進行管理。一個slabclass可以管理多個slab,它們的大小相同,由slabclass的trunk大小決定。slabclass內部有一個slot機制,用于快速分配和回收item。當item不再使用時,它會被放回slot的頭部,供后續(xù)分配使用,而不需要真正的釋放內存。

每個slabclass還與一個鏈表相關聯(lián),這個鏈表存儲了由該slabclass分配的所有item。新分配的item被放置在鏈表頭部,而鏈表中的item按照最近使用順序排列,這使得鏈表末尾的item是最久未使用的。這種機制為實現(xiàn)LRU(Least Recently Used)緩存策略提供了基礎。

為了快速查找和定位item,Memcached還使用了一個hash表。當需要查找或分配item時,hash表用于快速定位到相應的item,而鏈表則用于維護item的最近使用順序。

在item分配過程中,如果當前slabclass的鏈表中有過期的item,那么這些item會被優(yōu)先使用。如果沒有過期的item可用,系統(tǒng)會嘗試從slab中分配新的trunk。如果slab也用完,系統(tǒng)會向slabclass中添加新的slab。

值得注意的是,Memcached支持設置item的過期時間,但并不會定期檢查過期數據。只有在客戶端請求某個item時,Memcached才會檢查其過期時間,并在需要時返回錯誤。這種方法的優(yōu)點是減少了不必要的CPU開銷,但缺點是可能導致過期數據長時間占用內存。

為了處理并發(fā)訪問和數據一致性問題,Memcached采用了CAS(Compare-And-Swap)協(xié)議。每個item都有一個版本號,每次數據更新時,版本號會增加。當客戶端嘗試更新某個item時,它必須提供當前的版本號。只有當服務器端的版本號與客戶端提供的版本號一致時,更新操作才會被執(zhí)行。否則,服務器會返回錯誤,提示客戶端數據已經被其他進程修改。這種機制確保了數據的一致性和并發(fā)安全性。

Redis數據庫實現(xiàn)

Redis數據庫的功能確實比Memcached更為豐富,因為它不僅限于存儲字符串,還支持string(字符串)、list(列表)、set(集合)、sorted set(有序集合)和hash table(哈希表)這五種數據結構。這種靈活性使得Redis在處理復雜數據時更為高效。例如,如果要存儲一個人的信息,使用Redis的hash table結構,可以將人的名字作為key,然后將name和age等屬性作為field和value存儲。這樣,當只需要獲取某個特定屬性時,如年齡,Redis可以直接定位并返回該屬性的值,而不需要加載整個對象。

為了實現(xiàn)這些多樣化的數據結構,Redis引入了抽象對象的概念,稱為Redis Object。每個Redis Object都有一個類型標簽,可以是字符串、鏈表、集合、有序集合或哈希表之一。這種設計使得Redis能夠根據不同的使用場景選擇最適合的數據結構來實現(xiàn)。

為了提高效率,Redis還為每種數據類型準備了多種內部編碼方式(encoding),這些編碼方式是根據具體的使用情況和性能要求來選擇的。此外,Redis Object還包含了LRU(最近最少使用)信息,用于實現(xiàn)緩存淘汰策略。LRU信息記錄了對象上次被訪問的時間,與當前服務器維護的近似時間相比較,可以計算出對象的空閑時間。

Redis Object還引入了引用計數機制,用于共享對象和確定對象的刪除時間。通過引用計數,Redis可以追蹤對象的使用情況,并在適當的時候釋放內存。

最后,Redis Object使用了一個void*指針來指向對象的實際內容。這種設計使得Redis能夠以一種統(tǒng)一的方式來處理不同類型的對象,簡化了代碼邏輯。通過使用抽象對象,Redis實現(xiàn)了一種類似面向對象編程的風格,盡管其底層實現(xiàn)完全是基于C語言的。這種設計使得Redis的代碼結構清晰、易于理解和維護。

//#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可以是字符串、鏈表、集合、有序集合或哈希表,而key始終是字符串類型。這些高級數據結構在內部實現(xiàn)時,也會利用字符串作為基本元素。

在C語言中,沒有內置的字符串類型,因此Redis為了實現(xiàn)其強大的字符串處理能力,創(chuàng)建了一個名為SDS(Simple Dynamic String)的自定義字符串類型。SDS是一個簡單的結構體,其中包含三個字段:

  • len:表示當前字符串的實際長度。
  • free:表示當前字符串未使用空間的長度,即可以在不重新分配內存的情況下追加的字節(jié)數。
  • buf:一個字符數組,用于存儲字符串的實際內容。

通過len和free字段,SDS能夠高效地管理字符串的內存分配,避免了C語言原生字符串在處理長字符串或頻繁修改時可能遇到的性能瓶頸和內存浪費問題。同時,SDS還提供了一系列操作函數,如字符串拼接、長度計算等,使得字符串操作更加安全和便捷。

盡管Redis內部使用了復雜的數據結構,但所有這些結構都是以key-value對的形式存儲,并通過SDS來實現(xiàn)字符串的高效處理。這種設計使得Redis在保持簡潔和一致性的同時,能夠提供強大的數據存儲和操作能力。

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

在Redis中,key和value的關聯(lián)是通過哈希表(dictionary)來實現(xiàn)的。由于C語言本身沒有提供內置的字典數據結構,Redis自行實現(xiàn)了一個名為dict的字典結構。這個dict結構內部包含了一些關鍵的組件,如dictht(哈希表數組)和dictType(操作函數集合)。

dictht是dict結構中的哈希表數組,它實際上是一個包含多個哈希表的數組,通常包含兩個哈希表dictht[0]和dictht[1]。這樣做的主要目的是為了支持哈希表的動態(tài)擴容和縮容。當哈希表中的數據量增長到一定程度時,Redis會啟動擴容操作,將dictht[0]中的數據逐步遷移到dictht[1]中,同時會調整哈希表的大小以適應更多的數據。

dictType則是一個操作函數集合,它包含了處理哈希表中元素所需的各種函數指針,如哈希函數、比較函數、復制函數等。通過這些函數指針,Redis可以動態(tài)配置哈希表中元素的操作方法,從而實現(xiàn)靈活的鍵值對管理。

在dictht中,每個哈希表都由多個桶(bucket)組成,桶的數量由哈希表的大小決定。每個桶中存儲的是一個鏈表(dictEntry),鏈表中包含了具體的鍵值對。當插入或查找一個鍵時,Redis會根據鍵的哈希值和一個掩碼(sizemask)計算出對應的桶索引,然后在該桶的鏈表中進行查找或插入操作。

關于擴容和縮容操作,Redis采用了漸進式的方式。當需要擴容或縮容時,Redis并不會一次性完成所有的數據遷移工作,而是將這個過程分成多個階段。在每個階段中,Redis會在處理用戶請求的同時,順便遷移一部分數據。這樣做的好處是,可以避免一次性遷移數據導致的長時間延遲,從而保證了Redis的高性能。當然,在擴容或縮容期間,Redis的性能會受到一定的影響,因為每個操作都需要額外處理數據遷移。

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-value pair)的存儲容器。在 Redis 中,所有的數據都是以 key-value 對的形式存儲的,而每個 value 實際上是一個指向 redisObject 的指針,這個 redisObject 可以是 Redis 支持的五種數據類型(字符串、列表、集合、有序集合和哈希表)中的任何一種

5種type的對象,每一個都至少有兩種底層實現(xiàn)方式,以適應不同的使用場景和性能需求

  1. String(字符串):
  • REDIS_ENCODING_RAW: 最常見的實現(xiàn)方式,用于存儲任意長度的字符串。
  • REDIS_ENCODING_INT: 當字符串表示的是一個整數值,并且這個整數值在Redis能夠表示的范圍內時,Redis會使用這種編碼方式來存儲該字符串,這樣可以節(jié)省內存。
  • REDIS_ENCODING_EMBSTR: 當字符串長度較小時,Redis會使用embstr編碼,將字符串和對象頭部信息一起分配在一塊連續(xù)的內存中,以提高內存使用效率。

2. List(列表):

  • 普通雙向鏈表: 當列表元素較多時,Redis使用雙向鏈表來存儲列表元素,這樣可以在O(1)的時間復雜度內完成列表的頭部和尾部插入、刪除操作。
  • 壓縮鏈表(ziplist): 當列表元素較少時,Redis使用壓縮鏈表來存儲列表元素。壓縮鏈表是一種特殊的鏈表結構,它將節(jié)點之間的指針和節(jié)點的值存儲在一起,以節(jié)省內存空間。

3. Set(集合):

  • dict: 當集合中的元素不是整數時,Redis使用哈希表(dict)來實現(xiàn)集合,這樣可以快速判斷一個元素是否存在于集合中。
  • intset: 當集合中的元素全是整數時,Redis使用intset來實現(xiàn)集合,這樣可以更加高效地存儲和查找整數元素。

4. Sorted Set(有序集合):

  • skiplist(跳表): Redis使用跳表來實現(xiàn)有序集合,跳表是一種可以進行二分查找的有序鏈表,它可以在O(logN)的時間復雜度內完成元素的插入、刪除和查找操作。
  • ziplist: 當有序集合中的元素較少時,Redis也會使用壓縮鏈表來實現(xiàn)有序集合,以節(jié)省內存空間。

5. Hash(哈希表):

  • dict: Redis使用哈希表(dict)來實現(xiàn)哈希類型的數據結構,哈希表可以快速地根據鍵來查找對應的值。
  • ziplist: 當哈希表中的字段和值都比較小時,Redis也會使用壓縮鏈表來實現(xiàn)哈希表,以節(jié)省內存空間。

在Redis中,zset(有序集合)是通過結合skiplist(跳表)和ziplist(壓縮列表)來實現(xiàn)的。skiplist是一種可以進行對數級別查找的數據結構,它通過構建多級索引來實現(xiàn)快速查找、插入和刪除操作。而ziplist則是一種緊湊的、連續(xù)的內存結構,適用于存儲元素較少且元素值較小的情況。

當zset中的元素數量較少或者元素值較小時,Redis會使用ziplist來存儲元素和它們的分數(score)。在這種情況下,ziplist會按照分數的大小順序存儲元素,每個元素后面緊跟著它的分數。這種順序存儲的方式使得插入和刪除操作在內存分配方面相對高效。

然而,當zset中的元素數量超過一定閾值或者元素值過大時,ziplist可能會變得不再高效。在這種情況下,Redis會將ziplist轉換為skiplist,并使用一個額外的哈希表來存儲元素和它們的分數。哈希表允許Redis在O(1)時間復雜度內查找元素,而skiplist則提供了對數級別的查找性能。

Redis的數據結構

  • Hashtable(哈希表): Redis使用字典(dict)來實現(xiàn)哈希表。在Redis的字典中,每個dictEntry包含一個鍵(key)和一個值(value),它們都是字符串。字典的哈希表使用鏈地址法來解決哈希沖突,即當多個鍵具有相同的哈希值時,它們會被放置在同一個桶(bucket)中,形成一個鏈表。
  • Set(集合): Redis的集合也是通過字典來實現(xiàn)的。在集合的字典中,每個dictEntry的鍵(key)是集合中的一個元素,而值(value)通常是一個空值或占位符。這種設計使得Redis可以在O(1)時間復雜度內檢查一個元素是否存在于集合中。
  • Zset(有序集合): 如前所述,有序集合是通過結合skiplist和ziplist來實現(xiàn)的。skiplist提供了排序和快速查找的功能,而ziplist則用于在元素較少或元素值較小時進行高效存儲。當元素數量或元素值超過一定閾值時,Redis會將ziplist轉換為skiplist以保持性能。

此外,Redis還使用了一種稱為“expire dict”的字典來記錄每個鍵的過期時間。這個字典的鍵是數據庫中的鍵,而值是一個整數,表示該鍵的過期時間戳。Redis通過定期掃描這個字典來檢查鍵是否過期,并在必要時刪除它們。這種惰性刪除和定期掃描的機制有助于平衡內存使用和性能。

zset(有序集合)可以使用ziplist或skiplist作為底層數據結構來實現(xiàn)。ziplist(壓縮列表)是一種為節(jié)省內存而設計的連續(xù)內存塊結構,通常用于存儲小的字符串和整數,以及小的列表、哈希和有序集合。

當使用ziplist來存儲zset時,每個元素及其對應的score會連續(xù)地存儲在這個列表里。元素和score之間會有一定的編碼來區(qū)分它們。由于ziplist是緊湊存儲的,所以插入和刪除操作可能需要重新分配內存塊,并將相鄰的元素向前或向后移動以保持連續(xù)性。

在ziplist中,zset元素的存儲順序是根據它們的score來決定的。具有較小score的元素會被放置在列表的前面,而較大的score則會被放置在列表的后面。這樣,遍歷ziplist就可以按照score的順序訪問zset中的元素。

然而,當zset中的元素數量增加或者元素的字符串長度變得較長時,ziplist可能會變得不再高效。在這種情況下,Redis會選擇將zset 的底層數據結構從ziplist轉換為skiplist。skiplist是一種支持對數級別查找、插入和刪除操作的數據結構,它通過在原始鏈表的基礎上增加多級索引來實現(xiàn)快速查找

轉換過程大致如下:

  1. Redis會創(chuàng)建一個新的skiplist。
  2. 遍歷當前的ziplist,將每個元素及其score從ziplist中取出。
  3. 將取出的元素和score按照順序插入到新的skiplist中。
  4. 刪除舊的ziplist。
  5. 將zset的底層數據結構更新為新的skiplist。

這種轉換過程確保了zset在元素數量增加或元素值變大時仍然能夠保持高效的性能。通過使用ziplist和skiplist這兩種不同的數據結構,Redis能夠根據不同的使用場景來平衡內存使用和性能。

另外,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了。

簡單的來說。redis與Memcached在數據庫結構上的顯著不同就是:Memcached是一個簡單的內存緩存系統(tǒng),它沒有多個數據庫的概念,所有的數據都存儲在一個單一的內存空間中。這意味著在Memcached中,如果你嘗試存儲一個與現(xiàn)有鍵相同的鍵,新的值會覆蓋舊的值。

相反,Redis的設計更為靈活和強大。它默認提供了16個數據庫,編號從0到15,這些數據庫在邏輯上是完全隔離的。這意味著,你可以在不同的數據庫中存儲具有相同鍵名的數據,而這些數據在各自的數據庫中是完全獨立的。例如,在0號數據庫中有一個鍵叫"user:123",你可以在1號數據庫中再存儲一個同名鍵"user:123",而這兩個鍵的值和內容是完全不同的。

這種多數據庫的設計給了用戶更多的靈活性和選擇,特別是在需要隔離不同數據集或者想要使用鍵空間的不同部分進行不同的操作時。然而,需要注意的是,盡管這些數據庫在邏輯上是隔離的,但它們實際上都存儲在同一個Redis實例的內存中,因此仍然受到該實例的總內存限制。

Redis支持為數據設置過期時間。盡管我們在Redis對象的結構中并沒有直接看到保存expire時間的字段,但Redis實際上為每個數據庫額外維護了一個稱為"expire dict"的字典來專門記錄key的過期時間。

在"expire dict"中,每個鍵值對(dict entry)的鍵(key)對應著原始數據庫中的key,而值(value)則是一個64位的整數,表示該key的過期時間戳。當需要檢查一個key是否過期時,Redis會到這個"expire dict"中查找對應的過期時間戳,并與當前時間進行比較。

為什么Redis選擇這種方式來記錄過期時間呢?原因在于,并不是所有的key都需要設置過期時間。如果為每個key都保存一個過期時間字段,那么對于不設置過期時間的key來說,這將是一種空間的浪費。通過將過期時間單獨保存在"expire dict"中,Redis可以更加靈活地管理內存,只有在key實際設置了過期時間時才會為其在"expire dict"中創(chuàng)建相應的條目。

關于Redis的過期機制,它與Memcached有些類似,都采用了惰性刪除的策略。這意味著,當需要訪問一個key時,Redis會先檢查該key是否已過期。如果過期,則刪除該key并返回相應的錯誤。然而,僅僅依賴惰性刪除可能會導致內存浪費,因為過期的key可能長時間不被訪問,從而一直占據內存。

為了解決這個問題,Redis引入了定時任務"servercron"來輔助處理過期數據的刪除。這個函數會在一定的時間間隔內隨機選取"expire dict"中的一部分key進行檢查,如果它們已過期,則進行刪除。這個過程并不是一次性刪除所有過期的key,而是在每次執(zhí)行時隨機選取一部分進行處理,以確保不會對整個系統(tǒng)造成過大的負擔。此外,Redis還會根據系統(tǒng)的負載情況調整刪除操作的執(zhí)行時間,通常會在較短的時間內進行多次刪除操作,并在一定的時間間隔后進行一次較長時間的刪除操作,以平衡內存使用和系統(tǒng)性能

以上就是redis的數據的實現(xiàn),與memcached不同,redis還支持數據持久化

Redis數據庫持久化

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

RDB持久化

當用戶執(zhí)行save或bgsave命令時,Redis會觸發(fā)RDB(Redis DataBase)持久化操作。RDB持久化的核心理念是將Redis數據庫在某個時間點的完整快照保存到磁盤上的文件中。

在存儲過程中,RDB會遵循一定的格式:

  • 文件頭:首先寫入一個特定的字符串,作為RDB文件的標識符,這用于驗證文件是否為有效的RDB文件。
  • 版本信息:緊接著是Redis的版本信息,這有助于確定文件是否與當前的Redis版本兼容。
  • 數據庫列表:隨后是數據庫的實際內容。Redis會按照數據庫的編號順序(從0開始,一直到最大的編號)逐個保存每個數據庫的內容。這意味著,如果一個Redis實例配置了多個數據庫,并且它們都包含數據,那么這些數據庫的內容將按照編號順序連續(xù)存儲在RDB文件中。
  • 結束標志:當所有數據庫的內容都保存完畢后,會寫入一個特定的結束標志(如“EOF”),表示數據庫內容的結束。
  • 校驗和:最后,為了確保文件的完整性,RDB會計算文件的校驗和并存儲在文件末尾。這可以在后續(xù)加載文件時驗證數據的完整性。

圖片圖片

每個數據庫的數據存儲方式具有特定的結構。首先,Redis使用1字節(jié)的常量SELECTDB作為標識,表明接下來要切換至不同的數據庫。緊接著,SELECTDB之后是一個可變長度的字段,用于表示數據庫的編號。這個編號的長度不是固定的,它根據實際的數據庫編號大小而定,可能是1字節(jié)、2字節(jié)、3字節(jié)等,以確保能夠容納所有可能的數據庫編號。

完成數據庫編號的讀取后,接下來就是具體的key-value對數據了。這些數據按照key-value對的順序依次存儲,每個key-value對之間沒有明顯的分隔符,而是通過Redis的內部數據結構和編碼規(guī)則來區(qū)分。

圖片圖片

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,如下圖。

圖片圖片

RDB持久化過程中,rdbSaveObject函數會根據值的不同類型(val)采用不同的存儲策略。盡管最終的目標是將數據以字符串的形式存儲到文件中,但這個過程涉及到了多種數據類型和相應的編碼技巧。

對于簡單的數據類型,如字符串(strings)和整數(integers),Redis會直接將其轉換為字符串格式并存儲。對于更復雜的數據結構,如鏈表(linked lists)和哈希表(hash tables),Redis會采取一種更為結構化的方法。

對于鏈表,Redis首先會記錄整個鏈表的字節(jié)數,然后遍歷鏈表中的每個元素,將其逐個轉換為字符串并寫入文件。這樣做可以確保鏈表的結構在持久化過程中得以保留。

對于哈希表,Redis會先計算整個哈希表的字節(jié)數,然后遍歷其中的每個dictEntry。每個dictEntry的鍵(key)和值(value)都會被轉換為字符串并存儲到文件中。這樣,哈希表中的所有鍵值對都會在持久化過程中得到保留。

在存儲每個鍵值對時,如果設置了過期時間(expire time),Redis會首先將其存儲到文件中。接下來是值的類型信息,這對于后續(xù)的數據恢復至關重要。之后是鍵的字符串表示,最后是值的字符串表示。根據值的類型和底層實現(xiàn),Redis會使用不同的編碼技巧將其轉換為字符串格式。

為了實現(xiàn)數據壓縮和便于從文件中恢復數據,Redis采用了一系列編碼技巧。這些技巧包括使用特定的格式來存儲不同類型的數據,以及使用壓縮算法來減少文件的大小。通過這些方法,Redis能夠在保持數據完整性的同時,提高RDB文件的存儲效率和加載速度。

保存了RDB文件,當redis再啟動的時候,就根據RDB文件來恢復數據庫。由于以及在RDB文件中保存了數據庫的號碼,以及它包含的key-value對,以及每個key-value對中value的具體類型,實現(xiàn)方式,和數據,redis只要順序讀取文件,然后恢復object即可。由于保存了expire time,發(fā)現(xiàn)當前的時間已經比expire time大了,即數據已經超時了,則不恢復這個key-value對即可。

保存RDB文件是一個相對繁重的任務,因此Redis提供了后臺保存的機制來優(yōu)化這一過程。當執(zhí)行bgsave命令時,Redis會利用fork系統(tǒng)調用創(chuàng)建一個子進程。這個子進程是父進程的一個副本,它繼承了父進程當前的內存狀態(tài),包括Redis的數據庫。

由于子進程擁有父進程fork時的數據庫快照,它可以在不影響父進程的情況下獨立執(zhí)行保存操作。子進程會將這個數據庫快照寫入一個臨時文件(temp file)。在此過程中,Redis會追蹤對數據庫的修改次數(dirty count),這是為了確保在子進程保存期間的數據一致性。

一旦子進程完成了臨時文件的寫入,它會向父進程發(fā)送一個SIGUSR1信號。父進程在接收到這個信號后,會知道子進程已經完成了數據庫的保存工作。此時,父進程會安全地將這個臨時文件重命名為正式的RDB文件。這種重命名操作只有在子進程成功保存數據后才進行,從而確保了數據的完整性和安全性。

完成這一步驟后,父進程會記錄下這次保存的結束時間,并繼續(xù)提供正常的Redis數據庫服務。整個后臺保存過程對用戶來說是透明的,他們可以繼續(xù)進行數據庫操作,而不需要等待保存操作完成。這種后臺保存機制顯著提高了Redis的性能和響應能力。

這里有一個問題,在子進程保存期間,父進程的數據庫已經被修改了,而父進程只是記錄了修改的次數(dirty),被沒有進行修正操作。似乎使得RDB保存的不是實時的數據庫,不過AOF持久化,很好的解決了這個問題。

了用戶手動執(zhí)行SAVE或BGSAVE命令來觸發(fā)RDB持久化外,Redis還允許在配置文件中設置自動保存的條件。這些條件通?;趦蓚€因素:一是時間間隔(save配置項中的t),二是數據庫在這段時間內發(fā)生的修改次數(dirty)。

在Redis的配置文件中,可以通過save指令來設置自動保存規(guī)則。例如,save 900 1表示如果數據庫在900秒內至少有1次修改,則觸發(fā)一次后臺保存。類似地,save 300 10和save 60 10000分別表示在300秒內至少有10次修改,或者在60秒內至少有10000次修改時,也會觸發(fā)后臺保存。

Redis的服務器周期函數(serverCron)會定期檢查這些條件是否滿足。每當serverCron運行時,它會計算自上次RDB保存以來數據庫發(fā)生的修改次數(dirty count)以及距離上次保存的時間。如果這兩個條件都滿足任意一個save指令設置的要求,那么Redis就會執(zhí)行BGSAVE命令,啟動一個子進程來進行后臺保存操作。

值得注意的是,Redis確保在任何時刻都只有一個子進程在進行后臺保存操作。這是因為RDB保存是一個相對耗時的操作,特別是當涉及到大量的IO操作時。多個子進程同時進行大量的IO操作不僅可能導致性能下降,還可能使得IO管理變得復雜和不可預測。因此,Redis通過限制同時只有一個后臺保存進程來確保數據的一致性和系統(tǒng)的穩(wěn)定性。

AOF持久化

在考慮數據庫持久化時,不必總是遵循RDB的方式,即完整地保存當前數據庫的所有數據。另一種方法是記錄數據庫的變化過程,而不是結果狀態(tài)。這就是AOF(Append Only File)持久化的核心思想。與RDB不同,AOF不是保存數據庫的最終狀態(tài),而是保存了構建這個狀態(tài)所需的一系列命令。

AOF文件的內容格式相對簡單,它首先記錄每個命令的長度,然后緊接著是命令本身。這些命令是按照它們在數據庫中執(zhí)行的順序逐條保存的。這些命令不是隨機或無關的,而是由Redis客戶端發(fā)送給服務器的,用于修改數據庫狀態(tài)。

在Redis服務器中,有一個內部緩沖區(qū)aof_buf,當AOF持久化功能被啟用時,所有修改數據庫的命令都會被追加到這個緩沖區(qū)中。隨著事件的循環(huán),這些命令會被定期地寫入AOF文件。這個過程是通過flushAppendOnlyFile函數實現(xiàn)的,它會將aof_buf中的內容寫入到文件系統(tǒng)的內核緩沖區(qū)中,然后清空aof_buf以準備下一次的命令追加。這樣,只要AOF文件存在,并且其中的命令按序執(zhí)行,就可以完全重建數據庫的狀態(tài)。

值得注意的是,雖然命令被寫入了內核緩沖區(qū),但它們何時真正被物理寫入到磁盤上是由操作系統(tǒng)決定的。為了確保數據的可靠性,Redis提供了幾種同步策略供用戶選擇。例如,可以選擇每次寫入后都進行同步操作,這雖然會增加一些開銷,但可以提供更強的數據一致性保證。另一種策略是每秒同步一次,這通過后臺線程來實現(xiàn),避免了主線程的性能開銷。

與RDB不同,AOF持久化在寫入數據時不需要考慮同步的問題。因為RDB是一次性生成完整的數據庫快照,所以即使在生成過程中進行同步操作,對性能的影響也是有限的。而AOF則是持續(xù)不斷地追加命令,因此同步策略的選擇就顯得尤為重要。

除了實時追加命令到AOF文件外,Redis還提供了AOF重寫功能。這是一種優(yōu)化手段,通過讀取當前數據庫的狀態(tài),并生成相應的命令序列來重寫AOF文件。這樣做可以減小AOF文件的大小,提高重建數據庫的效率。AOF重寫也是在后臺進程中完成的,它讀取數據庫的當前狀態(tài),并生成相應的命令序列,然后寫入到一個臨時文件中。當重寫完成后,這個臨時文件會被替換成最終的AOF文件。

在AOF重寫期間,如果數據庫有新的更新操作,Redis會將這些操作保存在一個特殊的緩沖區(qū)中。當AOF重寫完成后,這些在重寫期間產生的更新命令會被追加到最終的AOF文件中,確保所有的數據庫變化都被完整地記錄下來。

最后,當Redis服務器啟動時,它會讀取AOF文件并執(zhí)行其中的命令,從而重建數據庫的狀態(tài)。為了執(zhí)行這些命令,Redis創(chuàng)建了一個虛擬的客戶端,這個客戶端沒有實際的網絡連接,但它擁有和真實客戶端相同的讀寫緩沖區(qū)。通過模擬客戶端的行為,Redis可以逐條執(zhí)行AOF文件中的命令,從而恢復數據庫到持久化時的狀態(tài)。

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


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


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

Redis的事務

Redis相較于memcached的另一個顯著優(yōu)勢在于其支持簡單的事務處理。事務,簡而言之,就是將多個命令組合在一起,一次性執(zhí)行。對于關系型數據庫而言,事務通常配備有回滾機制,意味著如果事務中的任何一條命令執(zhí)行失敗,整個事務都會回退到執(zhí)行前的狀態(tài)。然而,Redis的事務并不支持回滾功能,它僅保證命令按順序執(zhí)行,即使中途有命令失敗,也會繼續(xù)執(zhí)行后續(xù)命令。因此,Redis的事務被稱為“簡單事務”。

在Redis中執(zhí)行事務的過程相對直觀。首先,客戶端發(fā)送MULTI命令來標識事務的開始。隨后,客戶端輸入要執(zhí)行的一系列命令。最后,通過發(fā)送EXEC命令來執(zhí)行整個事務。當Redis服務器接收到MULTI命令時,它會將客戶端的狀態(tài)設置為REDIS_MULTI,表明該客戶端正在執(zhí)行事務。同時,服務器會在客戶端的multiState結構體中保存事務的具體信息,包括命令的數量和每個命令的具體內容。如果命令無法識別,服務器將不會保存這些命令。當收到EXEC命令時,Redis會按照multiState中保存的命令順序依次執(zhí)行它們,并記錄每個命令的返回值。如果在執(zhí)行過程中出現(xiàn)錯誤,Redis不會中斷事務,而是記錄錯誤信息并繼續(xù)執(zhí)行后續(xù)命令。當所有命令執(zhí)行完畢后,Redis會將所有命令的返回值一起返回給客戶端。

Redis之所以不支持回滾功能,部分原因是基于其設計哲學和性能考慮。傳統(tǒng)的關系型數據庫事務通常需要處理復雜的回滾邏輯,這可能會消耗大量的系統(tǒng)資源。而Redis作為一個內存數據庫,追求的是高性能和簡潔性。因此,它選擇不實現(xiàn)回滾機制,以換取更高的運行效率。同時,有觀點認為,如果事務中出現(xiàn)錯誤,通常是由于客戶端程序的問題,而不是服務器的問題。在這種情況下,要求服務器進行回滾可能并不合理。

我們知道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的發(fā)布訂閱頻道

Redis 支持頻道功能,允許用戶創(chuàng)建頻道并加入其中,形成一個類似于消息群組的機制。在頻道中,任何用戶發(fā)送的消息都會被頻道內的所有訂閱者接收到。

Redis 服務器內部維護了一個名為 pubsub_channels 的字典結構,用于存儲頻道與訂閱者之間的關系。字典的鍵是頻道的名稱,而值則是一個鏈表,其中包含了所有訂閱了該頻道的客戶端。每個客戶端也維護著自己的 pubsub_channels 列表,記錄了自己所關注的頻道。

當用戶在某個頻道中發(fā)布消息時,Redis 服務器會首先根據頻道名稱在 pubsub_channels 字典中查找對應的鏈表。一旦找到,服務器會遍歷鏈表中的所有客戶端,并將消息發(fā)送給它們。同樣地,當客戶端訂閱或取消訂閱某個頻道時,服務器僅需對 pubsub_channels 進行相應的操作即可。

除了普通頻道,Redis 還支持模式頻道,這是一種更靈活的消息訂閱方式。模式頻道使用正則表達式來匹配頻道名稱,當向某個普通頻道發(fā)送消息時,如果其名稱與某個模式頻道匹配,那么該消息不僅會被發(fā)送到普通頻道的訂閱者,還會被發(fā)送到與該模式頻道匹配的所有訂閱者。

在 Redis 服務器內部,模式頻道的實現(xiàn)依賴于一個名為 pubsub_patterns 的列表。這個列表存儲了多個 pubsubPattern 結構體,每個結構體都包含一個正則表達式和與之關聯(lián)的客戶端。與 pubsub_channels 不同,pubsub_patterns 的數量通常較少,因此使用簡單的列表結構就足夠了。

不過雖然 pubsub_patterns 列表存儲了與模式匹配的客戶端信息,但每個客戶端并不直接維護自己的 pubsub_patterns 列表。相反,客戶端在其內部維護了一個自己的模式頻道列表,這個列表僅包含客戶端所關注的模式頻道的名稱(以字符串形式存儲),而不是完整的 pubsubPattern 結構體。這樣的設計既簡化了客戶端的實現(xiàn),又有效地利用了內存資源。

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

當用戶向一個頻道發(fā)送消息時,Redis 服務器會首先查找 pubsub_channels 字典,確定哪些客戶端訂閱了該頻道,并將消息發(fā)送給這些客戶端。同時,服務器還會檢查 pubsub_patterns 列表,查找是否有與該頻道名稱匹配的模式頻道。如果找到匹配的模式頻道,服務器會進一步確定哪些客戶端訂閱了這些模式頻道,并將消息發(fā)送給這些客戶端。

值得注意的是,在這個過程中,如果有客戶端同時訂閱了某個普通頻道和與該頻道名稱匹配的模式頻道,那么該客戶端可能會收到重復的消息。這是因為 Redis 在發(fā)送消息時,并沒有進行去重處理。即,即使一個客戶端已經通過普通頻道接收到了消息,它仍然可能通過模式頻道再次接收到相同的消息。

這種設計選擇是基于這樣的考慮:Redis 認為處理消息去重的責任應該由客戶端程序來承擔,而不是由服務器來處理。這意味著,如果客戶端程序不希望接收到重復的消息,它需要在自己的邏輯中實現(xiàn)去重機制。這樣的設計使得 Redis 服務器可以更加高效地處理消息發(fā)布和訂閱操作,而不需要過多地考慮去重等復雜性問題。

因此,在使用 Redis 的發(fā)布/訂閱功能時,開發(fā)者需要注意這一點,并根據自己的需求在客戶端程序中實現(xià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;
}


責任編輯:武曉燕 來源: 步步運維步步坑
相關推薦

2023-12-05 15:24:46

2009-11-05 10:56:31

WCF通訊

2023-07-15 18:26:51

LinuxABI

2024-06-25 12:25:12

LangChain路由鏈

2009-11-05 14:53:54

Visual Stud

2021-10-19 07:27:08

HTTP代理網絡

2022-12-16 09:55:50

網絡架構OSI

2023-09-07 23:52:50

Flink代碼

2021-09-18 11:36:38

混沌工程云原生故障

2021-11-11 15:03:35

MySQLSQL索引

2021-11-07 23:46:32

MySQLSQL索引

2021-08-02 15:40:20

Java日志工具

2023-09-18 15:49:40

Ingress云原生Kubernetes

2024-02-21 21:19:18

切片Python語言

2009-11-06 16:05:37

WCF回調契約

2009-10-27 09:17:26

VB.NET生成靜態(tài)頁

2020-11-09 09:59:50

Ajax技術

2024-08-13 11:13:18

2009-10-26 15:45:43

VB.NET類構造

2009-11-06 10:25:34

WCF元數據交換
點贊
收藏

51CTO技術棧公眾號