16 | 高性能NoSQL

強(qiáng)大的 SQL 功能和 ACID 的屬性,使得關(guān)系數(shù)據(jù)庫廣泛應(yīng)用于各式各樣的系統(tǒng)中,但這并不意味著關(guān)系數(shù)據(jù)庫是完美的,關(guān)系數(shù)據(jù)庫存在如下缺點(diǎn):

存儲(chǔ)的是行記錄,無法存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)

以微博的關(guān)注關(guān)系為例,“我關(guān)注的人”是一個(gè)用戶 ID 列表,使用關(guān)系數(shù)據(jù)庫存儲(chǔ)只能將列表拆成多行,然后再查詢出來組裝,無法直接存儲(chǔ)一個(gè)列表。

schema 擴(kuò)展很不方便

關(guān)系數(shù)據(jù)庫的表結(jié)構(gòu) schema 是強(qiáng)約束,操作不存在的列會(huì)報(bào)錯(cuò),業(yè)務(wù)變化時(shí)擴(kuò)充列也比較麻煩,需要執(zhí)行 DDL(data definition language,如 CREATE、ALTER、DROP 等)語句修改,而且修改時(shí)可能會(huì)長時(shí)間鎖表(例如,MySQL 可能將表鎖住 1 個(gè)小時(shí))。

大數(shù)據(jù)場景下 I/O 較高

如果對(duì)一些大量數(shù)據(jù)的表進(jìn)行統(tǒng)計(jì)之類的運(yùn)算,關(guān)系數(shù)據(jù)庫的 I/O 會(huì)很高,因?yàn)榧词怪会槍?duì)其中某一列進(jìn)行運(yùn)算,關(guān)系數(shù)據(jù)庫也會(huì)將整行數(shù)據(jù)從存儲(chǔ)設(shè)備讀入內(nèi)存。

全文搜索功能比較弱

關(guān)系數(shù)據(jù)庫的全文搜索只能使用 like 進(jìn)行整表掃描匹配,性能非常低,在互聯(lián)網(wǎng)這種搜索復(fù)雜的場景下無法滿足業(yè)務(wù)要求。

針對(duì)上述問題,分別誕生了不同的 NoSQL 解決方案,這些方案與關(guān)系數(shù)據(jù)庫相比,在某些應(yīng)用場景下表現(xiàn)更好。但世上沒有免費(fèi)的午餐,NoSQL 方案帶來的優(yōu)勢(shì),本質(zhì)上是犧牲 ACID 中的某個(gè)或者某幾個(gè)特性,因此我們不能盲目地迷信 NoSQL 是銀彈,而應(yīng)該將 NoSQL 作為 SQL 的一個(gè)有力補(bǔ)充,NoSQL != No SQL,而是 NoSQL = Not Only SQL

常見的 NoSQL 方案分為 4 類。

K-V 存儲(chǔ):解決關(guān)系數(shù)據(jù)庫無法存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)的問題,以 Redis 為代表。

文檔數(shù)據(jù)庫:解決關(guān)系數(shù)據(jù)庫強(qiáng) schema 約束的問題,以 MongoDB 為代表。

列式數(shù)據(jù)庫:解決關(guān)系數(shù)據(jù)庫大數(shù)據(jù)場景下的 I/O 問題,以 HBase 為代表。

全文搜索引擎:解決關(guān)系數(shù)據(jù)庫的全文搜索性能問題,以 Elasticsearch 為代表。

今天,我來介紹一下各種高性能 NoSQL 方案的典型特征和應(yīng)用場景。

K-V 存儲(chǔ)

K-V 存儲(chǔ)的全稱是 Key-Value 存儲(chǔ),其中 Key 是數(shù)據(jù)的標(biāo)識(shí),和關(guān)系數(shù)據(jù)庫中的主鍵含義一樣,Value 就是具體的數(shù)據(jù)。

Redis 是 K-V 存儲(chǔ)的典型代表,它是一款開源(基于 BSD 許可)的高性能 K-V 緩存和存儲(chǔ)系統(tǒng)。Redis 的 Value 是具體的數(shù)據(jù)結(jié)構(gòu),包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,所以常常被稱為數(shù)據(jù)結(jié)構(gòu)服務(wù)器。

List 數(shù)據(jù)結(jié)構(gòu)為例,Redis 提供了下面這些典型的操作(更多請(qǐng)參考鏈接:http://redis.cn/commands.html#list):

LPOP key 從隊(duì)列的左邊出隊(duì)一個(gè)元素。

LINDEX key index 獲取一個(gè)元素,通過其索引列表。

LLEN key 獲得隊(duì)列(List)的長度。

RPOP key 從隊(duì)列的右邊出隊(duì)一個(gè)元素。

以上這些功能,如果用關(guān)系數(shù)據(jù)庫來實(shí)現(xiàn),就會(huì)變得很復(fù)雜。例如,LPOP 操作是移除并返回 key 對(duì)應(yīng)的 list 的第一個(gè)元素。如果用關(guān)系數(shù)據(jù)庫來存儲(chǔ),為了達(dá)到同樣目的,需要進(jìn)行下面的操作:

每條數(shù)據(jù)除了數(shù)據(jù)編號(hào)(例如,行 ID),還要有位置編號(hào),否則沒有辦法判斷哪條數(shù)據(jù)是第一條。注意這里不能用行 ID 作為位置編號(hào),因?yàn)槲覀儠?huì)往列表頭部插入數(shù)據(jù)。

查詢出第一條數(shù)據(jù)。

刪除第一條數(shù)據(jù)。

更新從第二條開始的所有數(shù)據(jù)的位置編號(hào)。

可以看出關(guān)系數(shù)據(jù)庫的實(shí)現(xiàn)很麻煩,而且需要進(jìn)行多次 SQL 操作,性能很低。

Redis 的缺點(diǎn)主要體現(xiàn)在并不支持完整的 ACID 事務(wù),Redis 雖然提供事務(wù)功能,但 Redis 的事務(wù)和關(guān)系數(shù)據(jù)庫的事務(wù)不可同日而語,Redis 的事務(wù)只能保證隔離性和一致性(I 和 C),無法保證原子性和持久性(A 和 D)。

雖然 Redis 并沒有嚴(yán)格遵循 ACID 原則,但實(shí)際上大部分業(yè)務(wù)也不需要嚴(yán)格遵循 ACID 原則。以上面的微博關(guān)注操作為例,即使系統(tǒng)沒有將 A 加入 B 的粉絲列表,其實(shí)業(yè)務(wù)影響也非常小,因此我們?cè)谠O(shè)計(jì)方案時(shí),需要根據(jù)業(yè)務(wù)特性和要求來確定是否可以用 Redis,而不能因?yàn)?Redis 不遵循 ACID 原則就直接放棄。

文檔數(shù)據(jù)庫

為了解決關(guān)系數(shù)據(jù)庫 schema 帶來的問題,文檔數(shù)據(jù)庫應(yīng)運(yùn)而生。文檔數(shù)據(jù)庫最大的特點(diǎn)就是 no-schema,可以存儲(chǔ)和讀取任意的數(shù)據(jù)。目前絕大部分文檔數(shù)據(jù)庫存儲(chǔ)的數(shù)據(jù)格式是 JSON(或者 BSON),因?yàn)?JSON 數(shù)據(jù)是自描述的,無須在使用前定義字段,讀取一個(gè) JSON 中不存在的字段也不會(huì)導(dǎo)致 SQL 那樣的語法錯(cuò)誤。

文檔數(shù)據(jù)庫的 no-schema 特性,給業(yè)務(wù)開發(fā)帶來了幾個(gè)明顯的優(yōu)勢(shì)。

1. 新增字段簡單

業(yè)務(wù)上增加新的字段,無須再像關(guān)系數(shù)據(jù)庫一樣要先執(zhí)行 DDL 語句修改表結(jié)構(gòu),程序代碼直接讀寫即可。

2. 歷史數(shù)據(jù)不會(huì)出錯(cuò)

對(duì)于歷史數(shù)據(jù),即使沒有新增的字段,也不會(huì)導(dǎo)致錯(cuò)誤,只會(huì)返回空值,此時(shí)代碼進(jìn)行兼容處理即可。

3. 可以很容易存儲(chǔ)復(fù)雜數(shù)據(jù)

JSON 是一種強(qiáng)大的描述語言,能夠描述復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。例如,我們?cè)O(shè)計(jì)一個(gè)用戶管理系統(tǒng),用戶的信息有 ID、姓名、性別、愛好、郵箱、地址、學(xué)歷信息。其中愛好是列表(因?yàn)榭梢杂卸鄠€(gè)愛好);地址是一個(gè)結(jié)構(gòu),包括省市區(qū)樓盤地址;學(xué)歷包括學(xué)校、專業(yè)、入學(xué)畢業(yè)年份信息等。如果我們用關(guān)系數(shù)據(jù)庫來存儲(chǔ),需要設(shè)計(jì)多張表,包括基本信息(列:ID、姓名、性別、郵箱)、愛好(列:ID、愛好)、地址(列:省、市、區(qū)、詳細(xì)地址)、學(xué)歷(列:入學(xué)時(shí)間、畢業(yè)時(shí)間、學(xué)校名稱、專業(yè)),而使用文檔數(shù)據(jù)庫,一個(gè) JSON 就可以全部描述。

{? ? ? ? ? ? ? ? ? ?

? ? "id": 10000,

? ? "name": "James",

? ? "sex": "male",

? ? "hobbies": [?

? ? ? ? "football",

? ? ? ? "playing",

? ? ? ? "singing"

? ? ],

? ? "email": "user@google.com",

? ? "address": {?

? ? ? ? "province": "GuangDong",

? ? ? ? "city": "GuangZhou",

? ? ? ? "district": "Tianhe",

? ? ? ? "detail": "PingYun Road 163"

? ? },

? ? "education": [?

? ? ? ? {?

? ? ? ? ? ? "begin": "2000-09-01",

? ? ? ? ? ? "end": "2004-07-01",

? ? ? ? ? ? "school": "UESTC",

? ? ? ? ? ? "major": "Computer Science & Technology"

? ? ? ? },

? ? ? ? {?

? ? ? ? ? ? "begin": "2004-09-01",

? ? ? ? ? ? "end": "2007-07-01",

? ? ? ? ? ? "school": "SCUT",

? ? ? ? ? ? "major": "Computer Science & Technology"

? ? ? ? }

? ? ]

}

通過這個(gè)樣例我們看到,使用 JSON 來描述數(shù)據(jù),比使用關(guān)系型數(shù)據(jù)庫表來描述數(shù)據(jù)方便和容易得多,而且更加容易理解。

文檔數(shù)據(jù)庫的這個(gè)特點(diǎn),特別適合電商和游戲這類的業(yè)務(wù)場景。以電商為例,不同商品的屬性差異很大。例如,冰箱的屬性和筆記本電腦的屬性差異非常大,如下圖所示。

即使是同類商品也有不同的屬性。例如,LCD 和 LED 顯示器,兩者有不同的參數(shù)指標(biāo)。這種業(yè)務(wù)場景如果使用關(guān)系數(shù)據(jù)庫來存儲(chǔ)數(shù)據(jù),就會(huì)很麻煩,而使用文檔數(shù)據(jù)庫,會(huì)簡單、方便許多,擴(kuò)展新的屬性也更加容易。

文檔數(shù)據(jù)庫 no-schema 的特性帶來的這些優(yōu)勢(shì)也是有代價(jià)的,最主要的代價(jià)就是不支持事務(wù)。例如,使用 MongoDB 來存儲(chǔ)商品庫存,系統(tǒng)創(chuàng)建訂單的時(shí)候首先需要減扣庫存,然后再創(chuàng)建訂單。這是一個(gè)事務(wù)操作,用關(guān)系數(shù)據(jù)庫來實(shí)現(xiàn)就很簡單,但如果用 MongoDB 來實(shí)現(xiàn),就無法做到事務(wù)性。異常情況下可能出現(xiàn)庫存被扣減了,但訂單沒有創(chuàng)建的情況。因此某些對(duì)事務(wù)要求嚴(yán)格的業(yè)務(wù)場景是不能使用文檔數(shù)據(jù)庫的。

文檔數(shù)據(jù)庫另外一個(gè)缺點(diǎn)就是無法實(shí)現(xiàn)關(guān)系數(shù)據(jù)庫的 join 操作。例如,我們有一個(gè)用戶信息表和一個(gè)訂單表,訂單表中有買家用戶 id。如果要查詢“購買了蘋果筆記本用戶中的女性用戶”,用關(guān)系數(shù)據(jù)庫來實(shí)現(xiàn),一個(gè)簡單的 join 操作就搞定了;而用文檔數(shù)據(jù)庫是無法進(jìn)行 join 查詢的,需要查兩次:一次查詢訂單表中購買了蘋果筆記本的用戶,然后再查詢這些用戶哪些是女性用戶。

列式數(shù)據(jù)庫

關(guān)系數(shù)據(jù)庫按照行式來存儲(chǔ)數(shù)據(jù),主要有以下幾個(gè)優(yōu)勢(shì)

業(yè)務(wù)同時(shí)讀取多個(gè)列時(shí)效率高,因?yàn)檫@些列都是按行存儲(chǔ)在一起的,一次磁盤操作就能夠把一行數(shù)據(jù)中的各個(gè)列都讀取到內(nèi)存中。

能夠一次性完成對(duì)一行中的多個(gè)列的寫操作,保證了針對(duì)行數(shù)據(jù)寫操作的原子性和一致性;否則如果采用列存儲(chǔ),可能會(huì)出現(xiàn)某次寫操作,有的列成功了,有的列失敗了,導(dǎo)致數(shù)據(jù)不一致。

我們可以看到,行式存儲(chǔ)的優(yōu)勢(shì)是在特定的業(yè)務(wù)場景下才能體現(xiàn),如果不存在這樣的業(yè)務(wù)場景,那么行式存儲(chǔ)的優(yōu)勢(shì)也將不復(fù)存在,甚至成為劣勢(shì),典型的場景就是海量數(shù)據(jù)進(jìn)行統(tǒng)計(jì)。例如,計(jì)算某個(gè)城市體重超重的人員數(shù)據(jù),實(shí)際上只需要讀取每個(gè)人的體重這一列并進(jìn)行統(tǒng)計(jì)即可,而行式存儲(chǔ)即使最終只使用一列,也會(huì)將所有行數(shù)據(jù)都讀取出來。如果單行用戶信息有 1KB,其中體重只有 4 個(gè)字節(jié),行式存儲(chǔ)還是會(huì)將整行 1KB 數(shù)據(jù)全部讀取到內(nèi)存中,這是明顯的浪費(fèi)。而如果采用列式存儲(chǔ),每個(gè)用戶只需要讀取 4 字節(jié)的體重?cái)?shù)據(jù)即可,I/O 將大大減少。

除了節(jié)省 I/O,列式存儲(chǔ)還具備更高的存儲(chǔ)壓縮比,能夠節(jié)省更多的存儲(chǔ)空間。普通的行式數(shù)據(jù)庫一般壓縮率 3:1 到 5:1 左右,而列式數(shù)據(jù)庫的壓縮率一般在 8:1 到 30:1 左右,因?yàn)閱蝹€(gè)列的數(shù)據(jù)相似度相比行來說更高,能夠達(dá)到更高的壓縮率。

同樣,如果場景發(fā)生變化,列式存儲(chǔ)的優(yōu)勢(shì)又會(huì)變成劣勢(shì)。典型的場景是需要頻繁地更新多個(gè)列。因?yàn)榱惺酱鎯?chǔ)將不同列存儲(chǔ)在磁盤上不連續(xù)的空間,導(dǎo)致更新多個(gè)列時(shí)磁盤是隨機(jī)寫操作;而行式存儲(chǔ)時(shí)同一行多個(gè)列都存儲(chǔ)在連續(xù)的空間,一次磁盤寫操作就可以完成,列式存儲(chǔ)的隨機(jī)寫效率要遠(yuǎn)遠(yuǎn)低于行式存儲(chǔ)的寫效率。此外,列式存儲(chǔ)高壓縮率在更新場景下也會(huì)成為劣勢(shì),因?yàn)楦聲r(shí)需要將存儲(chǔ)數(shù)據(jù)解壓后更新,然后再壓縮,最后寫入磁盤。

基于上述列式存儲(chǔ)的優(yōu)缺點(diǎn),一般將列式存儲(chǔ)應(yīng)用在離線的大數(shù)據(jù)分析和統(tǒng)計(jì)場景中,因?yàn)檫@種場景主要是針對(duì)部分列單列進(jìn)行操作,且數(shù)據(jù)寫入后就無須再更新刪除

全文搜索引擎

傳統(tǒng)的關(guān)系型數(shù)據(jù)庫通過索引來達(dá)到快速查詢的目的,但是在全文搜索的業(yè)務(wù)場景下,索引也無能為力,主要體現(xiàn)在:

全文搜索的條件可以隨意排列組合,如果通過索引來滿足,則索引的數(shù)量會(huì)非常多。

全文搜索的模糊匹配方式,索引無法滿足,只能用 like 查詢,而 like 查詢是整表掃描,效率非常低。

我舉一個(gè)具體的例子來看看關(guān)系型數(shù)據(jù)庫為何無法滿足全文搜索的要求。假設(shè)我們做一個(gè)婚戀網(wǎng)站,其主要目的是幫助程序員找朋友,但模式與傳統(tǒng)婚戀網(wǎng)站不同,是“程序員發(fā)布自己的信息,用戶來搜索程序員”。程序員的信息表設(shè)計(jì)如下:

我們來看一下這個(gè)簡單業(yè)務(wù)的搜索場景:

美女 1:聽說 PHP 是世界上最好的語言,那么 PHP 的程序員肯定是錢最多的,而且我媽一定要我找一個(gè)上海的。

美女 1 的搜索條件是“性別 + PHP + 上海”,其中“PHP”要用模糊匹配查詢“語言”列,“上海”要查詢“地點(diǎn)”列,如果用索引支撐,則需要建立“地點(diǎn)”這個(gè)索引。

美女 2:我好崇拜這些技術(shù)哥哥啊,要是能找一個(gè)鵝廠技術(shù)哥哥陪我旅游就更好了。

美女 2 的搜索條件是“性別 + 鵝廠 + 旅游”,其中“旅游”要用模糊匹配查詢“愛好”列,“鵝廠”需要查詢“單位”列,如果要用索引支撐,則需要建立“單位”索引。

美女 3:我是一個(gè)“女程序員”,想在北京找一個(gè)貓廠的 Java 技術(shù)專家。

美女 3 的搜索條件是“性別 + 貓廠 + 北京 + Java + 技術(shù)專家”,其中“貓廠 + 北京”可以通過索引來查詢,但“Java”“技術(shù)專家”都只能通過模糊匹配來查詢。

帥哥 4:程序員妹子有沒有漂亮的呢?試試看看。

帥哥 4 的搜索條件是“性別 + 美麗 + 美女”,只能通過模糊匹配搜索“自我介紹”列。

以上只是簡單舉個(gè)例子,實(shí)際上搜索條件是無法列舉完全的,各種排列組合非常多,通過這個(gè)簡單的樣例我們就可以看出關(guān)系數(shù)據(jù)庫在支撐全文搜索時(shí)的不足。

1. 全文搜索基本原理

全文搜索引擎的技術(shù)原理被稱為“倒排索引”(Inverted index),也常被稱為反向索引、置入檔案或反向檔案,是一種索引方法,其基本原理是建立單詞到文檔的索引。之所以被稱為“倒排”索引,是和“正排“索引相對(duì)的,“正排索引”的基本原理是建立文檔到單詞的索引。我們通過一個(gè)簡單的樣例來說明這兩種索引的差異。

假設(shè)我們有一個(gè)技術(shù)文章的網(wǎng)站,里面收集了各種技術(shù)文章,用戶可以在網(wǎng)站瀏覽或者搜索文章。

正排索引示例:

文章 ID文章名稱文章內(nèi)容

(注:文章內(nèi)容僅為示范,文章內(nèi)容實(shí)際上存儲(chǔ)的是幾千字的內(nèi)容。)

正排索引適用于根據(jù)文檔名稱來查詢文檔內(nèi)容。例如,用戶在網(wǎng)站上單擊了“面向?qū)ο罂▽毜涫鞘裁础保W(wǎng)站根據(jù)文章標(biāo)題查詢文章的內(nèi)容展示給用戶。

倒排索引示例:

(注:表格僅為示范,不是完整的倒排索引表格,實(shí)際上的倒排索引有成千上萬行,因?yàn)槊總€(gè)單詞就是一個(gè)索引。)

倒排索引適用于根據(jù)關(guān)鍵詞來查詢文檔內(nèi)容。例如,用戶只是想看“設(shè)計(jì)”相關(guān)的文章,網(wǎng)站需要將文章內(nèi)容中包含“設(shè)計(jì)”一詞的文章都搜索出來展示給用戶。

2. 全文搜索的使用方式

全文搜索引擎的索引對(duì)象是單詞和文檔,而關(guān)系數(shù)據(jù)庫的索引對(duì)象是鍵和行,兩者的術(shù)語差異很大,不能簡單地等同起來。因此,為了讓全文搜索引擎支持關(guān)系型數(shù)據(jù)的全文搜索,需要做一些轉(zhuǎn)換操作,即將關(guān)系型數(shù)據(jù)轉(zhuǎn)換為文檔數(shù)據(jù)。

目前常用的轉(zhuǎn)換方式是將關(guān)系型數(shù)據(jù)按照對(duì)象的形式轉(zhuǎn)換為 JSON 文檔,然后將 JSON 文檔輸入全文搜索引擎進(jìn)行索引。我同樣以程序員的基本信息表為例,看看如何轉(zhuǎn)換。

將前面樣例中的程序員表格轉(zhuǎn)換為 JSON 文檔,可以得到 3 個(gè)程序員信息相關(guān)的文檔,我以程序員 1 為例:

全文搜索引擎能夠基于 JSON 文檔建立全文索引,然后快速進(jìn)行全文搜索。以 Elasticsearch 為例,其索引基本原理如下:

Elastcisearch 是分布式的文檔存儲(chǔ)方式。它能存儲(chǔ)和檢索復(fù)雜的數(shù)據(jù)結(jié)構(gòu)——序列化成為 JSON 文檔——以實(shí)時(shí)的方式。

在 Elasticsearch 中,每個(gè)字段的所有數(shù)據(jù)都是默認(rèn)被索引的。即每個(gè)字段都有為了快速檢索設(shè)置的專用倒排索引。而且,不像其他多數(shù)的數(shù)據(jù)庫,它能在相同的查詢中使用所有倒排索引,并以驚人的速度返回結(jié)果。

https://www.elastic.co/guide/cn/elasticsearch/guide/current/data-in-data-out.html

小結(jié)

今天我為你講了為了彌補(bǔ)關(guān)系型數(shù)據(jù)庫缺陷而產(chǎn)生的 NoSQL 技術(shù),希望對(duì)你有所幫助。

這就是今天的全部內(nèi)容,留一道思考題給你吧,因?yàn)?NoSQL 的方案功能都很強(qiáng)大,有人認(rèn)為 NoSQL = No SQL,架構(gòu)設(shè)計(jì)的時(shí)候無需再使用關(guān)系數(shù)據(jù)庫,對(duì)此你怎么看?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容