天天看點

redis基本架構:一個鍵值資料庫包含什麼?

我們知道,Redis是典型的鍵值資料庫,是以今天,我準備手把手地帶你建構一個簡單的鍵值資料庫。為啥要這麼做呢?

還記得我在開篇詞說過嗎?Redis本身比較複雜,如果我們一上來就直接研究一個個具體的技術點,比如“單線程”“緩存”等,雖然可以直接學習到具體的内容,甚至立馬就能解決一些小問題,但是這樣學,很容易迷失在細枝末節裡。

從我自己的經驗來看,更好的學習方式就是先建立起“系統觀”。這也就是說,如果我們想要深入了解和優化Redis,就必須要對它的總體架構和關鍵子產品有一個全局的認知,然後再深入到具體的技術點。這也是我們這門課堅持的一種講課方式。

我相信,經過這樣一個過程,我們在實踐中定位和解決問題時,就會輕松很多,而且你還可以把這個學習方式遷移到其他的學習活動上。我希望你能徹底掌握這個學習思路,讓自己的學習、工作效率更高。

說遠了,還是回到我們今天的課程主題上。今天,在構造這個簡單的鍵值資料庫時,我們隻需要關注整體架構和核心子產品。這就相當于醫學上在正式解剖人體之前,會先解剖一隻小白鼠。我們通過剖析這個最簡單的鍵值資料庫,來迅速抓住學習和調優Redis的關鍵。

我把這個簡單的鍵值資料庫稱為SimpleKV。需要注意的是,GitHub上也有一個名為SimpleKV的項目,這跟我說的SimpleKV不是一回事,我說的隻是一個具有關鍵元件的鍵值資料庫架構。

好了,你是不是已經準備好了,那我們就一起來構造SimpleKV吧。

開始構造SimpleKV時,首先就要考慮裡面可以存什麼樣的資料,對資料可以做什麼樣的操作,也就是資料模型和操作接口。它們看似簡單,實際上卻是我們了解Redis經常被用于緩存、秒殺、分布式鎖等場景的重要基礎。

了解了資料模型,你就會明白,為什麼在有些場景下,原先使用關系型資料庫儲存的資料,也可以用鍵值資料庫儲存。例如,使用者資訊(使用者ID、姓名、年齡、性别等)通常用關系型資料庫儲存,在這個場景下,一個使用者ID對應一個使用者資訊集合,這就是鍵值資料庫的一種資料模型,它同樣能完成這一存儲需求。

但是,如果你隻知道資料模型,而不了解操作接口的話,可能就無法了解,為什麼在有些場景中,使用鍵值資料庫又不合适了。例如,同樣是在上面的場景中,如果你要對多個使用者的年齡計算均值,鍵值資料庫就無法完成了。因為它隻提供簡單的操作接口,無法支援複雜的聚合計算。

那麼,對于Redis來說,它到底能做什麼,不能做什麼呢?隻有先搞懂它的資料模型和操作接口,我們才能真正把“這塊好鋼用在刀刃上”。

接下來,我們就先來看可以存哪些資料。

可以存哪些資料?

對于鍵值資料庫而言,基本的資料模型是key-value模型。 例如,“hello”: “world”就是一個基本的KV對,其中,“hello”是key,“world”是value。SimpleKV也不例外。在SimpleKV中,key是String類型,而value是基本資料類型,例如String、整型等。

但是,SimpleKV畢竟是一個簡單的鍵值資料庫,對于實際生産環境中的鍵值資料庫來說,value類型還可以是複雜類型。

不同鍵值資料庫支援的key類型一般差異不大,而value類型則有較大差别。我們在對鍵值資料庫進行選型時,一個重要的考慮因素是它支援的value類型。例如,Memcached支援的value類型僅為String類型,而Redis支援的value類型包括了String、哈希表、清單、集合等。Redis能夠在實際業務場景中得到廣泛的應用,就是得益于支援多樣化類型的value。

從使用的角度來說,不同value類型的實作,不僅可以支撐不同業務的資料需求,而且也隐含着不同資料結構在性能、空間效率等方面的差異,進而導緻不同的value操作之間存在着差異。

隻有深入地了解了這背後的原理,我們才能在選擇Redis value類型和優化Redis性能時,做到遊刃有餘。

可以對資料做什麼操作?

知道了資料模型,接下來,我們就要看它對資料的基本操作了。SimpleKV是一個簡單的鍵值資料庫,是以,基本操作無外乎增删改查。

我們先來了解下SimpleKV需要支援的3種基本操作,即PUT、GET和DELETE。

  • PUT:新寫入或更新一個key-value對;
  • GET:根據一個key讀取相應的value值;
  • DELETE:根據一個key删除整個key-value對。

需要注意的是,有些鍵值資料庫的新寫/更新操作叫SET。新寫入和更新雖然是用一個操作接口,但在實際執行時,會根據key是否存在而執行相應的新寫或更新流程。

在實際的業務場景中,我們經常會碰到這種情況:查詢一個使用者在一段時間内的通路記錄。這種操作在鍵值資料庫中屬于SCAN操作,即根據一段key的範圍傳回相應的value值。是以,PUT/GET/DELETE/SCAN是一個鍵值資料庫的基本操作集合。

此外,實際業務場景通常還有更加豐富的需求,例如,在黑白名單應用中,需要判斷某個使用者是否存在。如果将該使用者的ID作為key,那麼,可以增加EXISTS操作接口,用于判斷某個key是否存在。對于一個具體的鍵值資料庫而言,你可以通過檢視操作文檔,了解其詳細的操作接口。

當然,當一個鍵值資料庫的value類型多樣化時,就需要包含相應的操作接口。例如,Redis的value有清單類型,是以它的接口就要包括對清單value的操作。後面我也會具體介紹,不同操作對Redis通路效率的影響。

說到這兒呢,資料模型和操作接口我們就構造完成了,這是我們的基礎工作。接下來呢,我們就要更進一步,考慮一個非常重要的設計問題:鍵值對儲存在記憶體還是外存?

儲存在記憶體的好處是讀寫很快,畢竟記憶體的通路速度一般都在百ns級别。但是,潛在的風險是一旦掉電,所有的資料都會丢失。

儲存在外存,雖然可以避免資料丢失,但是受限于磁盤的慢速讀寫(通常在幾ms級别),鍵值資料庫的整體性能會被拉低。

是以,如何進行設計選擇,我們通常需要考慮鍵值資料庫的主要應用場景。比如,緩存場景下的資料需要能快速通路但允許丢失,那麼,用于此場景的鍵值資料庫通常采用記憶體儲存鍵值資料。Memcached和Redis都是屬于記憶體鍵值資料庫。對于Redis而言,緩存是非常重要的一個應用場景。後面我會重點介紹Redis作為緩存使用的關鍵機制、優勢,以及常見的優化方法。

為了和Redis保持一緻,我們的SimpleKV就采用記憶體儲存鍵值資料。接下來,我們來了解下SimpleKV的基本元件。

大體來說,一個鍵值資料庫包括了通路架構、索引子產品、操作子產品和存儲子產品四部分(見下圖)。接下來,我們就從這四個部分入手,繼續建構我們的SimpleKV。

redis基本架構:一個鍵值資料庫包含什麼?

采用什麼通路模式?

通路模式通常有兩種:一種是通過函數庫調用的方式供外部應用使用,比如,上圖中的libsimplekv.so,就是以動态連結庫的形式連結到我們自己的程式中,提供鍵值存儲功能;另一種是通過網絡架構以Socket通信的形式對外提供鍵值對操作,這種形式可以提供廣泛的鍵值存儲服務。在上圖中,我們可以看到,網絡架構中包括Socket Server和協定解析。

不同的鍵值資料庫伺服器和用戶端互動的協定并不相同,我們在對鍵值資料庫進行二次開發、新增功能時,必須要了解和掌握鍵值資料庫的通信協定,這樣才能開發出相容的用戶端。

實際的鍵值資料庫也基本采用上述兩種方式,例如,RocksDB以動态連結庫的形式使用,而Memcached和Redis則是通過網絡架構通路。後面我還會給你介紹Redis現有的用戶端和通信協定。

通過網絡架構提供鍵值存儲服務,一方面擴大了鍵值資料庫的受用面,但另一方面,也給鍵值資料庫的性能、運作模型提供了不同的設計選擇,帶來了一些潛在的問題。

舉個例子,當用戶端發送一個如下的指令後,該指令會被封裝在網絡包中發送給鍵值資料庫:

PUT “hello” “world”      

鍵值資料庫網絡架構接收到網絡包,并按照相應的協定進行解析之後,就可以知道,用戶端想寫入一個鍵值對,并開始實際的寫入流程。此時,我們會遇到一個系統設計上的問題,簡單來說,就是網絡連接配接的處理、網絡請求的解析,以及資料存取的處理,是用一個線程、多個線程,還是多個程序來互動處理呢?該如何進行設計和取舍呢?我們一般把這個問題稱為I/O模型設計。不同的I/O模型對鍵值資料庫的性能和可擴充性會有不同的影響。

舉個例子,如果一個線程既要處理網絡連接配接、解析請求,又要完成資料存取,一旦某一步操作發生阻塞,整個線程就會阻塞住,這就降低了系統響應速度。如果我們采用不同線程處理不同操作,那麼,某個線程被阻塞時,其他線程還能正常運作。但是,不同線程間如果需要通路共享資源,那又會産生線程競争,也會影響系統效率,這又該怎麼辦呢?是以,這的确是個“兩難”選擇,需要我們進行精心的設計。

你可能經常聽說Redis是單線程,那麼,Redis又是如何做到“單線程,高性能”的呢?後面我再和你好好聊一聊。

如何定位鍵值對的位置?

當SimpleKV解析了用戶端發來的請求,知道了要進行的鍵值對操作,此時,SimpleKV需要查找所要操作的鍵值對是否存在,這依賴于鍵值資料庫的索引子產品。索引的作用是讓鍵值資料庫根據key找到相應value的存儲位置,進而執行操作。

索引的類型有很多,常見的有哈希表、B+樹、字典樹等。不同的索引結構在性能、空間消耗、并發控制等方面具有不同的特征。如果你看過其他鍵值資料庫,就會發現,不同鍵值資料庫采用的索引并不相同,例如,Memcached和Redis采用哈希表作為key-value索引,而RocksDB則采用跳表作為記憶體中key-value的索引。

一般而言,記憶體鍵值資料庫(例如Redis)采用哈希表作為索引,很大一部分原因在于,其鍵值資料基本都是儲存在記憶體中的,而記憶體的高性能随機通路特性可以很好地與哈希表O(1)的操作複雜度相比對。

SimpleKV的索引根據key找到value的存儲位置即可。但是,和SimpleKV不同,對于Redis而言,很有意思的一點是,它的value支援多種類型,當我們通過索引找到一個key所對應的value後,仍然需要從value的複雜結構(例如集合和清單)中進一步找到我們實際需要的資料,這個操作的效率本身就依賴于它們的實作結構。

Redis采用一些常見的高效索引結構作為某些value類型的底層資料結構,這一技術路線為Redis實作高性能通路提供了良好的支撐。

不同操作的具體邏輯是怎樣的?

SimpleKV的索引子產品負責根據key找到相應的value的存儲位置。對于不同的操作來說,找到存儲位置之後,需要進一步執行的操作的具體邏輯會有所差異。SimpleKV的操作子產品就實作了不同操作的具體邏輯:

  • 對于GET/SCAN操作而言,此時根據value的存儲位置傳回value值即可;
  • 對于PUT一個新的鍵值對資料而言,SimpleKV需要為該鍵值對配置設定記憶體空間;
  • 對于DELETE操作,SimpleKV需要删除鍵值對,并釋放相應的記憶體空間,這個過程由配置設定器完成。

不知道你注意到沒有,對于PUT和DELETE兩種操作來說,除了新寫入和删除鍵值對,還需要配置設定和釋放記憶體。這就不得不提SimpleKV的存儲子產品了。

如何實作重新開機後快速提供服務?

SimpleKV采用了常用的記憶體配置設定器glibc的malloc和free,是以,SimpleKV并不需要特别考慮記憶體空間的管理問題。但是,鍵值資料庫的鍵值對通常大小不一,glibc的配置設定器在處理随機的大小記憶體塊配置設定時,表現并不好。一旦儲存的鍵值對資料規模過大,就可能會造成較嚴重的記憶體碎片問題。

是以,配置設定器是鍵值資料庫中的一個關鍵因素。對于以記憶體存儲為主的Redis而言,這點尤為重要。Redis的記憶體配置設定器提供了多種選擇,配置設定效率也不一樣,後面我會具體講一講這個問題。

SimpleKV雖然依賴于記憶體儲存資料,提供快速通路,但是,我也希望SimpleKV重新開機後能快速重新提供服務,是以,我在SimpleKV的存儲子產品中增加了持久化功能。

不過,鑒于磁盤管理要比記憶體管理複雜,SimpleKV就直接采用了檔案形式,将鍵值資料通過調用本地檔案系統的操作接口儲存在磁盤上。此時,SimpleKV隻需要考慮何時将記憶體中的鍵值資料儲存到檔案中,就可以了。

一種方式是,對于每一個鍵值對,SimpleKV都對其進行落盤儲存,這雖然讓SimpleKV的資料更加可靠,但是,因為每次都要寫盤,SimpleKV的性能會受到很大影響。

另一種方式是,SimpleKV隻是周期性地把記憶體中的鍵值資料儲存到檔案中,這樣可以避免頻繁寫盤操作的性能影響。但是,一個潛在的代價是SimpleKV的資料仍然有丢失的風險。

和SimpleKV一樣,Redis也提供了持久化功能。不過,為了适應不同的業務場景,Redis為持久化提供了諸多的執行機制和優化改進,後面我會和你逐一介紹Redis在持久化機制中的關鍵設計考慮。

小結

至此,我們構造了一個簡單的鍵值資料庫SimpleKV。可以看到,前面兩步我們是從應用的角度進行設計的,也就是應用視角;後面四步其實就是SimpleKV完整的内部構造,可謂是麻雀雖小,五髒俱全。

SimpleKV包含了一個鍵值資料庫的基本元件,對這些元件有了了解之後,後面在學習Redis這個豐富版的SimpleKV時,就會輕松很多。

為了支援更加豐富的業務場景,Redis對這些元件或者功能進行了擴充,或者說是進行了精細優化,進而滿足了功能和性能等方面的要求。

  • Redis主要通過網絡架構進行通路,而不再是動态庫了,這也使得Redis可以作為一個基礎性的網絡服務進行通路,擴大了Redis的應用範圍。
  • Redis資料模型中的value類型很豐富,是以也帶來了更多的操作接口,例如面向清單的LPUSH/LPOP,面向集合的SADD/SREM等。在下節課,我将和你聊聊這些value模型背後的資料結構和操作效率,以及它們對Redis性能的影響。
  • Redis的持久化子產品能支援兩種方式:日志(AOF)和快照(RDB),這兩種持久化方式具有不同的優劣勢,影響到Redis的通路性能和可靠性。
  • SimpleKV是個簡單的單機鍵值資料庫,但是,Redis支援高可靠叢集和高可擴充叢集,是以,Redis中包含了相應的叢集功能支撐子產品。