目錄
一個鍵值數(shù)據(jù)庫的基本架構(gòu)包含什么?
// 從一個整體的視角觸發(fā),理清基本骨架
接下來,我會嘗試闡述構(gòu)建一個簡單鍵值數(shù)據(jù)庫的基本思路,暫且稱該庫為 。
開始構(gòu)造 時,首先就要考慮里面可以存什么樣的數(shù)據(jù),對數(shù)據(jù)可以做什么樣的操作,也就是數(shù)據(jù)模型和操作接口。它們看似簡單,實(shí)際上卻是我們理解 Redis 經(jīng)常被用于緩存、秒殺、分布式鎖等場景的重要基礎(chǔ)。
理解了數(shù)據(jù)模型,你就會明白,為什么在有些場景下,原先使用關(guān)系型數(shù)據(jù)庫保存的數(shù)據(jù),也可以用鍵值數(shù)據(jù)庫保存。例如,用戶信息(用戶 ID、姓名、年齡、性別等)通常用關(guān)系型數(shù)據(jù)庫保存,在這個場景下,一個用戶 ID 對應(yīng)一個用戶信息集合,這就是鍵值數(shù)據(jù)庫的一種數(shù)據(jù)模型,它同樣能完成這一存儲需求。
但是,如果你只知道數(shù)據(jù)模型,而不了解操作接口的話,可能就無法理解,為什么在有些場景中,使用鍵值數(shù)據(jù)庫又不合適了。例如,同樣是在上面的場景中,如果你要對多個用戶的年齡計算均值,鍵值數(shù)據(jù)庫就無法完成了。因?yàn)樗惶峁┖唵蔚牟僮鹘涌冢瑹o法支持復(fù)雜的聚合計算。
那么,對于 Redis 來說,它到底能做什么,不能做什么呢?只有先搞懂它的數(shù)據(jù)模型和操作接口,我們才能真正把“這塊好鋼用在刀刃上”。
接下來,我們就先來看可以存哪些數(shù)據(jù)。
1、可以存儲哪些數(shù)據(jù)?
對于鍵值數(shù)據(jù)庫而言,基本的數(shù)據(jù)模型是 key-value 模型。 例如,“hello”: “world”就是一個基本的 KV 對,其中,“hello”是 key,“world”是 value。 也不例外。在 中,key 是 類型,而 value 是基本數(shù)據(jù)類型,例如 、整型等。
但是, 畢竟是一個簡單的鍵值數(shù)據(jù)庫,對于實(shí)際生產(chǎn)環(huán)境中的鍵值數(shù)據(jù)庫來說,value 類型還可以是復(fù)雜類型。
不同鍵值數(shù)據(jù)庫支持的 key 類型一般差異不大,而 value 類型則有較大差別。我們在對鍵值數(shù)據(jù)庫進(jìn)行選型時,一個重要的考慮因素是它支持的 value 類型。例如, 支持的 value 類型僅為 類型,而 Redis 支持的 value 類型包括了 、哈希表、列表、集合等。Redis 能夠在實(shí)際業(yè)務(wù)場景中得到廣泛的應(yīng)用,就是得益于支持多樣化類型的 value。
從使用的角度來說,不同 value 類型的實(shí)現(xiàn),不僅可以支撐不同業(yè)務(wù)的數(shù)據(jù)需求,而且也隱含著不同數(shù)據(jù)結(jié)構(gòu)在性能、空間效率等方面的差異,從而導(dǎo)致不同的 value 操作之間存在著差異。
只有深入地理解了這背后的原理,我們才能在選擇 Redis value 類型和優(yōu)化 Redis 性能時,做到游刃有余。
2、可以對數(shù)據(jù)做什么操作?如何存儲?
知道了數(shù)據(jù)模型,接下來,我們就要看它對數(shù)據(jù)的基本操作了。 是一個簡單的鍵值數(shù)據(jù)庫,因此,基本操作無外乎增刪改查。
我們先來了解下 需要支持的 3 種基本操作,即 PUT、GET 和 。
新寫入和更新雖然是用一個操作接口,但在實(shí)際執(zhí)行時鍵值數(shù)據(jù)庫有哪些,會根據(jù) key 是否存在而執(zhí)行相應(yīng)的新寫或更新流程。
在實(shí)際的業(yè)務(wù)場景中,我們經(jīng)常會碰到這種情況:查詢一個用戶在一段時間內(nèi)的訪問記錄。這種操作在鍵值數(shù)據(jù)庫中屬于 SCAN 操作,即根據(jù)一段 key 的范圍返回相應(yīng)的 value 值。因此,PUT/GET//SCAN 是一個鍵值數(shù)據(jù)庫的基本操作集合。
此外,實(shí)際業(yè)務(wù)場景通常還有更加豐富的需求,例如,在黑白名單應(yīng)用中,需要判斷某個用戶是否存在。如果將該用戶的 ID 作為 key,那么,可以增加 操作接口,用于判斷某個 key 是否存在。
對于一個具體的鍵值數(shù)據(jù)庫而言,你可以通過查看操作文檔,了解其詳細(xì)的操作接口。當(dāng)然,當(dāng)一個鍵值數(shù)據(jù)庫的 value 類型多樣化時,就需要包含相應(yīng)的操作接口。例如,Redis 的 value 有列表類型,因此它的接口就要包括對列表 value 的操作。
至此,數(shù)據(jù)模型和操作接口我們就構(gòu)造完成了,這是我們的基礎(chǔ)工作。接下來呢,我們就要更進(jìn)一步,考慮一個非常重要的設(shè)計問題:鍵值對保存在內(nèi)存還是外存?
因此,如何進(jìn)行設(shè)計選擇,我們通常需要考慮鍵值數(shù)據(jù)庫的主要應(yīng)用場景。比如,緩存場景下的數(shù)據(jù)需要能快速訪問但允許丟失,那么,用于此場景的鍵值數(shù)據(jù)庫通常采用內(nèi)存保存鍵值數(shù)據(jù)。
和 Redis 都是屬于內(nèi)存鍵值數(shù)據(jù)庫。對于 Redis 而言,緩存是非常重要的一個應(yīng)用場景。為了和 Redis 保持一致,我們的 就采用內(nèi)存保存鍵值數(shù)據(jù)。
接下來,我們來了解下 的基本組件。
大體來說,一個鍵值數(shù)據(jù)庫包括了訪問框架、索引模塊、操作模塊和存儲模塊四部分(見下圖)。接下來,我們就從這四個部分入手鍵值數(shù)據(jù)庫有哪些,繼續(xù)構(gòu)建我們的 。
3、采用什么樣的訪問方式?
訪問模式通常有兩種:一種是通過函數(shù)庫調(diào)用的方式供外部應(yīng)用使用,比如,上圖中的 .so,就是以動態(tài)鏈接庫的形式鏈接到我們自己的程序中,提供鍵值存儲功能;另一種是通過網(wǎng)絡(luò)框架以 通信的形式對外提供鍵值對操作,這種形式可以提供廣泛的鍵值存儲服務(wù)。
在上圖中,我們可以看到,網(wǎng)絡(luò)框架中包括 和協(xié)議解析。不同的鍵值數(shù)據(jù)庫服務(wù)器和客戶端交互的協(xié)議并不相同,我們在對鍵值數(shù)據(jù)庫進(jìn)行二次開發(fā)、新增功能時,必須要了解和掌握鍵值數(shù)據(jù)庫的通信協(xié)議,這樣才能開發(fā)出兼容的客戶端。
實(shí)際的鍵值數(shù)據(jù)庫也基本采用上述兩種方式,例如, 以動態(tài)鏈接庫的形式使用,而 和 Redis 則是通過網(wǎng)絡(luò)框架訪問。
通過網(wǎng)絡(luò)框架提供鍵值存儲服務(wù),一方面擴(kuò)大了鍵值數(shù)據(jù)庫的受用面,但另一方面,也給鍵值數(shù)據(jù)庫的性能、運(yùn)行模型提供了不同的設(shè)計選擇,帶來了一些潛在的問題。
舉個例子,當(dāng)客戶端發(fā)送一個如下的命令后,該命令會被封裝在網(wǎng)絡(luò)包中發(fā)送給鍵值數(shù)據(jù)庫:
PUT hello world
鍵值數(shù)據(jù)庫網(wǎng)絡(luò)框架接收到網(wǎng)絡(luò)包,并按照相應(yīng)的協(xié)議進(jìn)行解析之后,就可以知道,客戶端想寫入一個鍵值對,并開始實(shí)際的寫入流程。此時,我們會遇到一個系統(tǒng)設(shè)計上的問題,簡單來說,就是網(wǎng)絡(luò)連接的處理、網(wǎng)絡(luò)請求的解析,以及數(shù)據(jù)存取的處理,是用一個線程、多個線程,還是多個進(jìn)程來交互處理呢?該如何進(jìn)行設(shè)計和取舍呢?我們一般把這個問題稱為 I/O 模型設(shè)計。不同的 I/O 模型對鍵值數(shù)據(jù)庫的性能和可擴(kuò)展性會有不同的影響。
舉個例子,如果一個線程既要處理網(wǎng)絡(luò)連接、解析請求,又要完成數(shù)據(jù)存取,一旦某一步操作發(fā)生阻塞,整個線程就會阻塞住,這就降低了系統(tǒng)響應(yīng)速度。如果我們采用不同線程處理不同操作,那么,某個線程被阻塞時,其他線程還能正常運(yùn)行。但是,不同線程間如果需要訪問共享資源,那又會產(chǎn)生線程競爭,也會影響系統(tǒng)效率,這又該怎么辦呢?所以,這的確是個“兩難”選擇,需要我們進(jìn)行精心的設(shè)計。
你可能經(jīng)常聽說 Redis 是單線程,那么,Redis 又是如何做到“單線程,高性能”的呢?
4、如何定位鍵值對的位置?
當(dāng) 解析了客戶端發(fā)來的請求,知道了要進(jìn)行的鍵值對操作,此時, 需要查找所要操作的鍵值對是否存在,這依賴于鍵值數(shù)據(jù)庫的索引模塊。索引的作用是讓鍵值數(shù)據(jù)庫根據(jù) key 找到相應(yīng) value 的存儲位置,進(jìn)而執(zhí)行操作。
索引的類型有很多,常見的有哈希表、B+ 樹、字典樹等。不同的索引結(jié)構(gòu)在性能、空間消耗、并發(fā)控制等方面具有不同的特征。如果你看過其他鍵值數(shù)據(jù)庫,就會發(fā)現(xiàn),不同鍵值數(shù)據(jù)庫采用的索引并不相同,例如, 和 Redis 采用哈希表作為 key-value 索引,而 則采用跳表作為內(nèi)存中 key-value 的索引。
一般而言,內(nèi)存鍵值數(shù)據(jù)庫(例如 Redis)采用哈希表作為索引,很大一部分原因在于,其鍵值數(shù)據(jù)基本都是保存在內(nèi)存中的,而內(nèi)存的高性能隨機(jī)訪問特性可以很好地與哈希表 O(1) 的操作復(fù)雜度相匹配。// hash表->提供高效率的隨機(jī)訪問
的索引根據(jù) key 找到 value 的存儲位置即可。但是,和 不同,對于 Redis 而言,很有意思的一點(diǎn)是,它的 value 支持多種類型,當(dāng)我們通過索引找到一個 key 所對應(yīng)的 value 后,仍然需要從 value 的復(fù)雜結(jié)構(gòu)(例如集合和列表)中進(jìn)一步找到我們實(shí)際需要的數(shù)據(jù),這個操作的效率本身就依賴于它們的實(shí)現(xiàn)結(jié)構(gòu)。
Redis 采用一些常見的高效索引結(jié)構(gòu)作為某些 value 類型的底層數(shù)據(jù)結(jié)構(gòu),這一技術(shù)路線為 Redis 實(shí)現(xiàn)高性能訪問提供了良好的支撐。
5、不同操作的具體邏輯是怎樣的?
的索引模塊負(fù)責(zé)根據(jù) key 找到相應(yīng)的 value 的存儲位置。對于不同的操作來說,找到存儲位置之后,需要進(jìn)一步執(zhí)行的操作的具體邏輯會有所差異。 的操作模塊就實(shí)現(xiàn)了不同操作的具體邏輯:
不知道你注意到?jīng)]有,對于 PUT 和 兩種操作來說,除了新寫入和刪除鍵值對,還需要分配和釋放內(nèi)存。這就不得不提 的存儲模塊了。// 數(shù)據(jù)庫需要對內(nèi)存進(jìn)行合理分配
6、如何實(shí)現(xiàn)重啟后快速提供服務(wù)?
采用了常用的內(nèi)存分配器 glibc 的 和 free(() 在運(yùn)行期動態(tài)分配分配內(nèi)存,free()釋放由其分配的內(nèi)存。),因此, 并不需要特別考慮內(nèi)存空間的管理問題。但是,鍵值數(shù)據(jù)庫的鍵值對通常大小不一,glibc 的分配器在處理隨機(jī)的大小內(nèi)存塊分配時,表現(xiàn)并不好。一旦保存的鍵值對數(shù)據(jù)規(guī)模過大,就可能會造成較嚴(yán)重的內(nèi)存碎片問題。
因此,分配器是鍵值數(shù)據(jù)庫中的一個關(guān)鍵因素。對于以內(nèi)存存儲為主的 Redis 而言,這點(diǎn)尤為重要。Redis 的內(nèi)存分配器提供了多種選擇,分配效率也不一樣,后面將會闡述這個問題。
雖然依賴于內(nèi)存保存數(shù)據(jù),提供快速訪問,但是,我也希望 重啟后能快速重新提供服務(wù),所以,我在 的存儲模塊中增加了持久化功能。
不過,鑒于磁盤管理要比內(nèi)存管理復(fù)雜, 就直接采用了文件形式,將鍵值數(shù)據(jù)通過調(diào)用本地文件系統(tǒng)的操作接口保存在磁盤上。此時, 只需要考慮何時將內(nèi)存中的鍵值數(shù)據(jù)保存到文件中,就可以了。// 以文件形式保存數(shù)據(jù)持久化到磁盤中->內(nèi)存
// 至此又會引出數(shù)據(jù)丟失和讀寫效率的折中問題
和 一樣,Redis 也提供了持久化功能。不過,為了適應(yīng)不同的業(yè)務(wù)場景,Redis 為持久化提供了諸多的執(zhí)行機(jī)制和優(yōu)化改進(jìn),后面將逐一介紹 Redis 在持久化機(jī)制中的關(guān)鍵設(shè)計考慮。
總結(jié)
至此,我們構(gòu)造了一個簡單的鍵值數(shù)據(jù)庫 。可以看到,前面兩步我們是從應(yīng)用的角度進(jìn)行設(shè)計的,也就是應(yīng)用視角;后面四步其實(shí)就是 完整的內(nèi)部構(gòu)造,可謂是麻雀雖小,五臟俱全。
包含了一個鍵值數(shù)據(jù)庫的基本組件,對這些組件有了了解之后,后面在學(xué)習(xí) Redis 這個豐富版的 時,就會輕松很多。// 從骨架開始,理清脈絡(luò)
為了支持更加豐富的業(yè)務(wù)場景,Redis 對這些組件或者功能進(jìn)行了擴(kuò)展,或者說是進(jìn)行了精細(xì)優(yōu)化,從而滿足了功能和性能等方面的要求。
從這張對比圖中,我們可以看到,從 演進(jìn)到 Redis,有以下幾個重要變化:
通過對 的構(gòu)建,相信你已經(jīng)對鍵值數(shù)據(jù)庫的基本結(jié)構(gòu)和重要模塊有了整體認(rèn)知和深刻理解,這其實(shí)也是 Redis 單機(jī)版的核心基礎(chǔ)。