咱們知道,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。
須要注意的是,有些鍵值數據庫的新寫/更新操做叫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。
訪問模式一般有兩種:一種是經過函數庫調用的方式供外部應用使用,好比,上圖中的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的操做模塊就實現了不一樣操做的具體邏輯:
不知道你注意到沒有,對於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對這些組件或者功能進行了擴展,或者說是進行了精細優化,從而知足了功能和性能等方面的要求。
從這張對比圖中,咱們能夠看到,從SimpleKV演進到Redis,有如下幾個重要變化:
經過這節課SimpleKV的構建,我相信你已經對鍵值數據庫的基本結構和重要模塊有了總體認知和深入理解,這其實也是Redis單機版的核心基礎。針對剛剛提到的幾點Redis的重大演進,在接下來的課程中,我會依次進行重點講解。與此同時,我還會結合實戰場景,讓你不只可以理解原理,還能真正學以至用,提高實戰能力。
給你留個小問題:和你瞭解的Redis相比,你以爲,SimpleKV裏面還缺乏什麼功能組件或模塊嗎?
歡迎在留言區寫下你的思考和答案,咱們一塊兒交流討論,也歡迎你把今天的內容分享給你的朋友。