偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

綜述

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

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

服務(wù)方式

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

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

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

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

事件模型

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

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

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

圖片圖片

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

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

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

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

由于文件描述符在同一時(shí)間內(nèi)是唯一的,Redis利用了這一特性,采用了一個(gè)簡單而直接的方法來處理連接信息。它使用一個(gè)數(shù)組,其中文件描述符(fd)直接作為數(shù)組的索引。這樣,當(dāng)Redis需要查找與特定文件描述符相關(guān)聯(lián)的客戶端信息時(shí),它只需簡單地使用該文件描述符作為數(shù)組的索引,從而直接訪問到相應(yīng)的客戶端信息。這種方法的查找效率達(dá)到了O(1),因?yàn)樗苊饬藦?fù)雜的搜索算法或數(shù)據(jù)結(jié)構(gòu)的開銷。

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

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

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

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

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

內(nèi)存分配

memcached和redis的核心任務(wù)都是在內(nèi)存中操作數(shù)據(jù),所以內(nèi)存管理才是最核心的內(nèi)容。

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

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

由于Redis沒有自己的內(nèi)存池,其內(nèi)存申請和釋放操作變得十分直接和方便,只需使用標(biāo)準(zhǔn)的malloc和free函數(shù)即可。而Memcached的內(nèi)存管理則相對復(fù)雜,因?yàn)樗枰S護(hù)自己的內(nèi)存池,并在分配和釋放內(nèi)存時(shí)進(jìn)行相應(yīng)的管理操作。具體的實(shí)現(xiàn)細(xì)節(jié)和復(fù)雜性將在后續(xù)關(guān)于Memcached的slab機(jī)制的講解中進(jìn)一步分析。

數(shù)據(jù)庫實(shí)現(xiàn)

Memcached數(shù)據(jù)庫實(shí)現(xiàn)

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

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

圖片圖片

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

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

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

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

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

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

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

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

圖片圖片

圖片圖片

圖片圖片

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

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

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

為了快速查找和定位item,Memcached還使用了一個(gè)hash表。當(dāng)需要查找或分配item時(shí),hash表用于快速定位到相應(yīng)的item,而鏈表則用于維護(hù)item的最近使用順序。

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

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

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

Redis數(shù)據(jù)庫實(shí)現(xiàn)

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

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

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

Redis Object還引入了引用計(jì)數(shù)機(jī)制,用于共享對象和確定對象的刪除時(shí)間。通過引用計(jì)數(shù),Redis可以追蹤對象的使用情況,并在適當(dāng)?shù)臅r(shí)候釋放內(nèi)存。

最后,Redis Object使用了一個(gè)void*指針來指向?qū)ο蟮膶?shí)際內(nèi)容。這種設(shè)計(jì)使得Redis能夠以一種統(tǒng)一的方式來處理不同類型的對象,簡化了代碼邏輯。通過使用抽象對象,Redis實(shí)現(xiàn)了一種類似面向?qū)ο缶幊痰娘L(fēng)格,盡管其底層實(shí)現(xiàn)完全是基于C語言的。這種設(shè)計(jì)使得Redis的代碼結(jié)構(gòu)清晰、易于理解和維護(hù)。

//#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的數(shù)據(jù),
                                                // 可   以采用不同的存儲方式
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;         // 引用計(jì)數(shù)
    void *ptr;
} robj;

說到底redis還是一個(gè)key-value的數(shù)據(jù)庫,無論它支持多么復(fù)雜的數(shù)據(jù)結(jié)構(gòu),最終都是以key-value對的形式進(jìn)行存儲。這些value可以是字符串、鏈表、集合、有序集合或哈希表,而key始終是字符串類型。這些高級數(shù)據(jù)結(jié)構(gòu)在內(nèi)部實(shí)現(xiàn)時(shí),也會利用字符串作為基本元素。

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

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

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

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

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

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

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

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

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

關(guān)于擴(kuò)容和縮容操作,Redis采用了漸進(jìn)式的方式。當(dāng)需要擴(kuò)容或縮容時(shí),Redis并不會一次性完成所有的數(shù)據(jù)遷移工作,而是將這個(gè)過程分成多個(gè)階段。在每個(gè)階段中,Redis會在處理用戶請求的同時(shí),順便遷移一部分?jǐn)?shù)據(jù)。這樣做的好處是,可以避免一次性遷移數(shù)據(jù)導(dǎo)致的長時(shí)間延遲,從而保證了Redis的高性能。當(dāng)然,在擴(kuò)容或縮容期間,Redis的性能會受到一定的影響,因?yàn)槊總€(gè)操作都需要額外處理數(shù)據(jù)遷移。

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ù)來的時(shí)候,通過計(jì)算出的hashkey, 讓hashkey & sizemask來確定它要放的桶的位置
    // 當(dāng)size取2^n的時(shí)候,sizemask就是1...111,這樣就和hashkey % size有一樣的效果,但是使用&會快很多。這就是原因
    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ù)庫就好實(shí)現(xiàn)了。所有數(shù)據(jù)讀存儲在dict中,它作為鍵值對(key-value pair)的存儲容器。在 Redis 中,所有的數(shù)據(jù)都是以 key-value 對的形式存儲的,而每個(gè) value 實(shí)際上是一個(gè)指向 redisObject 的指針,這個(gè) redisObject 可以是 Redis 支持的五種數(shù)據(jù)類型(字符串、列表、集合、有序集合和哈希表)中的任何一種

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

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

2. List(列表):

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

3. Set(集合):

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

4. Sorted Set(有序集合):

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

5. Hash(哈希表):

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

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

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

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

Redis的數(shù)據(jù)結(jié)構(gòu)

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

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

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

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

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

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

轉(zhuǎn)換過程大致如下:

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

這種轉(zhuǎn)換過程確保了zset在元素?cái)?shù)量增加或元素值變大時(shí)仍然能夠保持高效的性能。通過使用ziplist和skiplist這兩種不同的數(shù)據(jù)結(jié)構(gòu),Redis能夠根據(jù)不同的使用場景來平衡內(nèi)存使用和性能。

另外,ziplist如何實(shí)現(xiàn)hashtable呢?其實(shí)也很簡單,就是存儲一個(gè)key,存儲一個(gè)value,再存儲一個(gè)key,再存儲一個(gè)value。還是順序存儲,與zset實(shí)現(xiàn)類似,所以當(dāng)元素超過一定數(shù)量,或者某個(gè)元素的字符數(shù)超過一定數(shù)量時(shí),就會轉(zhuǎn)換成hashtable來實(shí)現(xiàn)。各種底層實(shí)現(xiàn)方式是可以轉(zhuǎn)換的,redis可以根據(jù)情況選擇最合適的實(shí)現(xiàn)方式,這也是這樣使用類似面向?qū)ο蟮膶?shí)現(xiàn)方式的好處

需要指出的是,使用skiplist來實(shí)現(xiàn)zset的時(shí)候,其實(shí)還用了一個(gè)dict,這個(gè)dict存儲一樣的鍵值對。為什么呢?因?yàn)閟kiplist的查找只是lgn的(可能變成n),而dict可以到O(1), 所以使用一個(gè)dict來加速查找,由于skiplist和dict可以指向同一個(gè)redis object,所以不會浪費(fèi)太多內(nèi)存。另外使用ziplist實(shí)現(xiàn)zset的時(shí)候,為什么不用dict來加速查找呢?因?yàn)閦iplist支持的元素個(gè)數(shù)很少(個(gè)數(shù)多時(shí)就轉(zhuǎn)換成skiplist了),順序遍歷也很快,所以不用dict了。

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

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

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

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

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

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

關(guān)于Redis的過期機(jī)制,它與Memcached有些類似,都采用了惰性刪除的策略。這意味著,當(dāng)需要訪問一個(gè)key時(shí),Redis會先檢查該key是否已過期。如果過期,則刪除該key并返回相應(yīng)的錯誤。然而,僅僅依賴惰性刪除可能會導(dǎo)致內(nèi)存浪費(fèi),因?yàn)檫^期的key可能長時(shí)間不被訪問,從而一直占據(jù)內(nèi)存。

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

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

Redis數(shù)據(jù)庫持久化

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

RDB持久化

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

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

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

圖片圖片

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

完成數(shù)據(jù)庫編號的讀取后,接下來就是具體的key-value對數(shù)據(jù)了。這些數(shù)據(jù)按照key-value對的順序依次存儲,每個(gè)key-value對之間沒有明顯的分隔符,而是通過Redis的內(nèi)部數(shù)據(jù)結(jié)構(gòu)和編碼規(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;
}

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

圖片圖片

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

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

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

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

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

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

保存了RDB文件,當(dāng)redis再啟動的時(shí)候,就根據(jù)RDB文件來恢復(fù)數(shù)據(jù)庫。由于以及在RDB文件中保存了數(shù)據(jù)庫的號碼,以及它包含的key-value對,以及每個(gè)key-value對中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對即可。

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

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

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

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

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

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

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

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

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

AOF持久化

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

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

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

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

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

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

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

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

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


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


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

Redis的事務(wù)

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

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

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

我們知道redis是單event loop的,在真正執(zhí)行一個(gè)事物的時(shí)候(即redis收到exec命令后),事物的執(zhí)行過程是不會被打斷的,所有命令都會在一個(gè)event loop中執(zhí)行完。但是在用戶逐個(gè)輸入事務(wù)的命令的時(shí)候,這期間,可能已經(jīng)有別的客戶修改了事務(wù)里面用到的數(shù)據(jù),這就可能產(chǎn)生問題。

所以redis還提供了watch命令,用戶可以在輸入multi之前,執(zhí)行watch命令,指定需要觀察的數(shù)據(jù),這樣如果在exec之前,有其他的客戶端修改了這些被watch的數(shù)據(jù),則exec的時(shí)候,執(zhí)行到處理被修改的數(shù)據(jù)的命令的時(shí)候,會執(zhí)行失敗,提示數(shù)據(jù)已經(jīng)dirty。這是如何是實(shí)現(xiàn)的呢?原來在每一個(gè)redisDb中還有一個(gè)dict watched_keys,watched_kesy中dictentry的key是被watch的數(shù)據(jù)庫的key,而value則是一個(gè)list,里面存儲的是watch它的client。

同時(shí),每個(gè)client也有一個(gè)watched_keys,里面保存的是這個(gè)client當(dāng)前watch的key。在執(zhí)行watch的時(shí)候,redis在對應(yīng)的數(shù)據(jù)庫的watched_keys中找到這個(gè)key(如果沒有,則新建一個(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í)候,首先會檢查是否被設(shè)置了REDIS_DIRTY_CAS,如果是,則表明數(shù)據(jù)dirty了,事務(wù)無法執(zhí)行,會立即返回錯誤,只有client沒有被設(shè)置REDIS_DIRTY_CAS的時(shí)候才能夠執(zhí)行事務(wù)。需要指出的是,執(zhí)行exec后,該client的所有watch的key都會被清除,同時(shí)db中該key的client列表也會清除該client,即執(zhí)行exec后,該client不再watch任何key(即使exec沒有執(zhí)行成功也是一樣)。所以說redis的事務(wù)是簡單的事務(wù),算不上真正的事務(wù)。

Redis的發(fā)布訂閱頻道

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

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

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

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

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

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

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

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

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

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

因此,在使用 Redis 的發(fā)布/訂閱功能時(shí),開發(fā)者需要注意這一點(diǎn),并根據(jù)自己的需求在客戶端程序中實(shí)現(xiàn)適當(dāng)?shù)娜ブ貦C(jī)制,以確保消息的正確性和一致性。

/* 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;
}


責(zé)任編輯:武曉燕 來源: 步步運(yùn)維步步坑
相關(guān)推薦

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代理網(wǎng)絡(luò)

2023-09-07 23:52:50

Flink代碼

2022-12-16 09:55:50

網(wǎng)絡(luò)架構(gòu)OSI

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日志工具

2024-02-21 21:19:18

切片Python語言

2009-10-27 09:17:26

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

2009-11-06 16:05:37

WCF回調(diào)契約

2024-08-13 11:13:18

2023-09-18 15:49:40

Ingress云原生Kubernetes

2020-11-09 09:59:50

Ajax技術(shù)

2024-09-23 17:05:44

2009-10-26 15:45:43

VB.NET類構(gòu)造
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號