16 | 高性能NoSQL

此爲筆記

課程鏈接

https://time.geekbang.org/column/intro/100006601?utm_source=time_web&utm_medium=menu&utm_term=timewebmenu


關係數據庫經過幾十年的發展後已經非常成熟,強大的 SQL 功能和 ACID 的屬性,使得關係數據庫廣泛應用於各式各樣的系統中,但這並不意味着關係數據庫是完美的,關係數據庫存在如下缺點。

  • 關係數據庫存儲的是行記錄,無法存儲數據結構

以微博的關注關係爲例,“我關注的人”是一個用戶 ID 列表,使用關係數據庫存儲只能將列表拆成多行,然後再查詢出來組裝,無法直接存儲一個列表。

  • 關係數據庫的 schema 擴展很不方便

關係數據庫的表結構 schema 是強約束,操作不存在的列會報錯,業務變化時擴充列也比較麻煩,需要執行 DDL(data definition language,如 CREATE、ALTER、DROP 等)語句修改,而且修改時可能會長時間鎖表(例如,MySQL 可能將表鎖住 1 個小時)。

  • 關係數據庫在大數據場景下 I/O 較高

如果對一些大量數據的表進行統計之類的運算,關係數據庫的 I/O 會很高,因爲即使只針對其中某一列進行運算,關係數據庫也會將整行數據從存儲設備讀入內存。

  • 關係數據庫的全文搜索功能比較弱

關係數據庫的全文搜索只能使用 like 進行整表掃描匹配,性能非常低,在互聯網這種搜索複雜的場景下無法滿足業務要求。

針對上述問題,分別誕生了不同的 NoSQL 解決方案,這些方案與關係數據庫相比,在某些應用場景下表現更好。但世上沒有免費的午餐,NoSQL 方案帶來的優勢,本質上是犧牲 ACID 中的某個或者某幾個特性,因此我們不能盲目地迷信 NoSQL 是銀彈,而應該將 NoSQL 作爲 SQL 的一個有力補充,NoSQL != No SQL,而是 NoSQL = Not Only SQL。

常見的 NoSQL 方案分爲 4 類。

  • K-V 存儲:解決關係數據庫無法存儲數據結構的問題,以 Redis 爲代表。

  • 文檔數據庫:解決關係數據庫強 schema 約束的問題,以 MongoDB 爲代表。

  • 列式數據庫:解決關係數據庫大數據場景下的 I/O 問題,以 HBase 爲代表。

  • 全文搜索引擎:解決關係數據庫的全文搜索性能問題,以 Elasticsearch 爲代表。

今天,我來介紹一下各種高性能 NoSQL 方案的典型特徵和應用場景。

K-V 存儲

K-V 存儲的全稱是 Key-Value 存儲,其中 Key 是數據的標識,和關係數據庫中的主鍵含義一樣,Value 就是具體的數據。

Redis 是 K-V 存儲的典型代表,它是一款開源(基於 BSD 許可)的高性能 K-V 緩存和存儲系統。Redis 的 Value 是具體的數據結構,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,所以常常被稱爲數據結構服務器。

以 List 數據結構爲例,Redis 提供了下面這些典型的操作(更多請參考鏈接:http://redis.cn/commands.html#list):

  • LPOP key 從隊列的左邊出隊一個元素。

  • LINDEX key index 獲取一個元素,通過其索引列表。

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

  • RPOP key 從隊列的右邊出隊一個元素。

以上這些功能,如果用關係數據庫來實現,就會變得很複雜。例如,LPOP 操作是移除並返回 key 對應的 list 的第一個元素。如果用關係數據庫來存儲,爲了達到同樣目的,需要進行下面的操作:

  • 每條數據除了數據編號(例如,行 ID),還要有位置編號,否則沒有辦法判斷哪條數據是第一條。注意這裏不能用行 ID 作爲位置編號,因爲我們會往列表頭部插入數據。

  • 查詢出第一條數據。

  • 刪除第一條數據。

  • 更新從第二條開始的所有數據的位置編號。

可以看出關係數據庫的實現很麻煩,而且需要進行多次 SQL 操作,性能很低。

Redis 的缺點主要體現在並不支持完整的 ACID 事務,Redis 雖然提供事務功能,但 Redis 的事務和關係數據庫的事務不可同日而語,Redis 的事務只能保證隔離性和一致性(I 和 C),無法保證原子性和持久性(A 和 D)。

雖然 Redis 並沒有嚴格遵循 ACID 原則,但實際上大部分業務也不需要嚴格遵循 ACID 原則。以上面的微博關注操作爲例,即使系統沒有將 A 加入 B 的粉絲列表,其實業務影響也非常小,因此我們在設計方案時,需要根據業務特性和要求來確定是否可以用 Redis,而不能因爲 Redis 不遵循 ACID 原則就直接放棄。

文檔數據庫

爲了解決關係數據庫 schema 帶來的問題,文檔數據庫應運而生。文檔數據庫最大的特點就是 no-schema,可以存儲和讀取任意的數據。目前絕大部分文檔數據庫存儲的數據格式是 JSON(或者 BSON),因爲 JSON 數據是自描述的,無須在使用前定義字段,讀取一個 JSON 中不存在的字段也不會導致 SQL 那樣的語法錯誤。

文檔數據庫的 no-schema 特性,給業務開發帶來了幾個明顯的優勢。

1. 新增字段簡單

業務上增加新的字段,無須再像關係數據庫一樣要先執行 DDL 語句修改表結構,程序代碼直接讀寫即可。

2. 歷史數據不會出錯

對於歷史數據,即使沒有新增的字段,也不會導致錯誤,只會返回空值,此時代碼進行兼容處理即可。

3. 可以很容易存儲複雜數據

JSON 是一種強大的描述語言,能夠描述複雜的數據結構。例如,我們設計一個用戶管理系統,用戶的信息有 ID、姓名、性別、愛好、郵箱、地址、學歷信息。其中愛好是列表(因爲可以有多個愛好);地址是一個結構,包括省市區樓盤地址;學歷包括學校、專業、入學畢業年份信息等。如果我們用關係數據庫來存儲,需要設計多張表,包括基本信息(列:ID、姓名、性別、郵箱)、愛好(列:ID、愛好)、地址(列:省、市、區、詳細地址)、學歷(列:入學時間、畢業時間、學校名稱、專業),而使用文檔數據庫,一個 JSON 就可以全部描述。


 
 

{

 

"id": 10000,

 

"name": "James",

 

"sex": "male",

 

"hobbies": [

 

"football",

 

"playing",

 

"singing"

 

],

 

"email": "[email protected]",

 

"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"

 

}

 

]

 

}

複製代碼

通過這個樣例我們看到,使用 JSON 來描述數據,比使用關係型數據庫表來描述數據方便和容易得多,而且更加容易理解。

文檔數據庫的這個特點,特別適合電商和遊戲這類的業務場景。以電商爲例,不同商品的屬性差異很大。例如,冰箱的屬性和筆記本電腦的屬性差異非常大,如下圖所示。

即使是同類商品也有不同的屬性。例如,LCD 和 LED 顯示器,兩者有不同的參數指標。這種業務場景如果使用關係數據庫來存儲數據,就會很麻煩,而使用文檔數據庫,會簡單、方便許多,擴展新的屬性也更加容易。

文檔數據庫 no-schema 的特性帶來的這些優勢也是有代價的,最主要的代價就是不支持事務。例如,使用 MongoDB 來存儲商品庫存,系統創建訂單的時候首先需要減扣庫存,然後再創建訂單。這是一個事務操作,用關係數據庫來實現就很簡單,但如果用 MongoDB 來實現,就無法做到事務性。異常情況下可能出現庫存被扣減了,但訂單沒有創建的情況。因此某些對事務要求嚴格的業務場景是不能使用文檔數據庫的。

文檔數據庫另外一個缺點就是無法實現關係數據庫的 join 操作。例如,我們有一個用戶信息表和一個訂單表,訂單表中有買家用戶 id。如果要查詢“購買了蘋果筆記本用戶中的女性用戶”,用關係數據庫來實現,一個簡單的 join 操作就搞定了;而用文檔數據庫是無法進行 join 查詢的,需要查兩次:一次查詢訂單表中購買了蘋果筆記本的用戶,然後再查詢這些用戶哪些是女性用戶。

列式數據庫

顧名思義,列式數據庫就是按照列來存儲數據的數據庫,與之對應的傳統關係數據庫被稱爲“行式數據庫”,因爲關係數據庫是按照行來存儲數據的。

關係數據庫按照行式來存儲數據,主要有以下幾個優勢:

  • 業務同時讀取多個列時效率高,因爲這些列都是按行存儲在一起的,一次磁盤操作就能夠把一行數據中的各個列都讀取到內存中。

  • 能夠一次性完成對一行中的多個列的寫操作,保證了針對行數據寫操作的原子性和一致性;否則如果採用列存儲,可能會出現某次寫操作,有的列成功了,有的列失敗了,導致數據不一致。

我們可以看到,行式存儲的優勢是在特定的業務場景下才能體現,如果不存在這樣的業務場景,那麼行式存儲的優勢也將不復存在,甚至成爲劣勢,典型的場景就是海量數據進行統計。例如,計算某個城市體重超重的人員數據,實際上只需要讀取每個人的體重這一列並進行統計即可,而行式存儲即使最終只使用一列,也會將所有行數據都讀取出來。如果單行用戶信息有 1KB,其中體重只有 4 個字節,行式存儲還是會將整行 1KB 數據全部讀取到內存中,這是明顯的浪費。而如果採用列式存儲,每個用戶只需要讀取 4 字節的體重數據即可,I/O 將大大減少。

除了節省 I/O,列式存儲還具備更高的存儲壓縮比,能夠節省更多的存儲空間。普通的行式數據庫一般壓縮率在 3:1 到 5:1 左右,而列式數據庫的壓縮率一般在 8:1 到 30:1 左右,因爲單個列的數據相似度相比行來說更高,能夠達到更高的壓縮率。

同樣,如果場景發生變化,列式存儲的優勢又會變成劣勢。典型的場景是需要頻繁地更新多個列。因爲列式存儲將不同列存儲在磁盤上不連續的空間,導致更新多個列時磁盤是隨機寫操作;而行式存儲時同一行多個列都存儲在連續的空間,一次磁盤寫操作就可以完成,列式存儲的隨機寫效率要遠遠低於行式存儲的寫效率。此外,列式存儲高壓縮率在更新場景下也會成爲劣勢,因爲更新時需要將存儲數據解壓後更新,然後再壓縮,最後寫入磁盤。

基於上述列式存儲的優缺點,一般將列式存儲應用在離線的大數據分析和統計場景中,因爲這種場景主要是針對部分列單列進行操作,且數據寫入後就無須再更新刪除。

全文搜索引擎

傳統的關係型數據庫通過索引來達到快速查詢的目的,但是在全文搜索的業務場景下,索引也無能爲力,主要體現在:

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

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

我舉一個具體的例子來看看關係型數據庫爲何無法滿足全文搜索的要求。假設我們做一個婚戀網站,其主要目的是幫助程序員找朋友,但模式與傳統婚戀網站不同,是“程序員發佈自己的信息,用戶來搜索程序員”。程序員的信息表設計如下:

ID 姓名 性別 地點 單位 愛好 語言 自我介紹
1 多隆 北京 貓廠 寫代碼、旅遊、馬拉松 Java、C++、PHP 技術專家,簡單,爲人熱情  
2 如花 上海 鵝廠 旅遊、美食、唱歌 PHP、Java 美女如花,風華絕代,貌美如花  
3 小寶 廣州 熊廠 泡吧、踢球 Python、Go、C 我是一匹來自北方的狼  

我們來看一下這個簡單業務的搜索場景:

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

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

  • 美女 2:我好崇拜這些技術哥哥啊,要是能找一個鵝廠技術哥哥陪我旅遊就更好了。

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

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

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

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

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

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

1. 全文搜索基本原理

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

假設我們有一個技術文章的網站,裏面收集了各種技術文章,用戶可以在網站瀏覽或者搜索文章。

正排索引示例:

文章 ID 文章名稱 文章內容
1 敏捷架構設計原則 省略具體內容,文檔內容包含:架構、設計、架構師等單詞
2 Java 編程必知必會 省略具體內容,文檔內容包含:Java、編程、面向對象、類、架構、設計等單詞
3 面向對象葵花寶典是什麼 省略具體內容,文檔內容包含:設計、模式、對象、類、Java 等單詞

(注:文章內容僅爲示範,文章內容實際上存儲的是幾千字的內容。)

正排索引適用於根據文檔名稱來查詢文檔內容。例如,用戶在網站上單擊了“面向對象葵花寶典是什麼”,網站根據文章標題查詢文章的內容展示給用戶。

倒排索引示例:

單詞 文檔 ID 列表
架構 1,2
設計 1,2,3
Java 2,3

(注:表格僅爲示範,不是完整的倒排索引表格,實際上的倒排索引有成千上萬行,因爲每個單詞就是一個索引。)

倒排索引適用於根據關鍵詞來查詢文檔內容。例如,用戶只是想看“設計”相關的文章,網站需要將文章內容中包含“設計”一詞的文章都搜索出來展示給用戶。

2. 全文搜索的使用方式

全文搜索引擎的索引對象是單詞和文檔,而關係數據庫的索引對象是鍵和行,兩者的術語差異很大,不能簡單地等同起來。因此,爲了讓全文搜索引擎支持關係型數據的全文搜索,需要做一些轉換操作,即將關係型數據轉換爲文檔數據。

目前常用的轉換方式是將關係型數據按照對象的形式轉換爲 JSON 文檔,然後將 JSON 文檔輸入全文搜索引擎進行索引。我同樣以程序員的基本信息表爲例,看看如何轉換。

將前面樣例中的程序員表格轉換爲 JSON 文檔,可以得到 3 個程序員信息相關的文檔,我以程序員 1 爲例:


 
 

{

 

"id": 1,

 

" 姓名 ": " 多隆 ",

 

" 性別 ": " 男 ",

 

" 地點 ": " 北京 ",

 

" 單位 ": " 貓廠 ",

 

" 愛好 ": " 寫代碼,旅遊,馬拉松 ",

 

" 語言 ": "Java、C++、PHP",

 

" 自我介紹 ": " 技術專家,簡單,爲人熱情 "

 

}

複製代碼

全文搜索引擎能夠基於 JSON 文檔建立全文索引,然後快速進行全文搜索。以 Elasticsearch 爲例,其索引基本原理如下:

Elastcisearch 是分佈式的文檔存儲方式。它能存儲和檢索複雜的數據結構——序列化成爲 JSON 文檔——以實時的方式。

在 Elasticsearch 中,每個字段的所有數據都是默認被索引的。即每個字段都有爲了快速檢索設置的專用倒排索引。而且,不像其他多數的數據庫,它能在相同的查詢中使用所有倒排索引,並以驚人的速度返回結果。

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

小結

今天我爲你講了爲了彌補關係型數據庫缺陷而產生的 NoSQL 技術,希望對你有所幫助。

這就是今天的全部內容,留一道思考題給你吧,因爲 NoSQL 的方案功能都很強大,有人認爲 NoSQL = No SQL,架構設計的時候無需再使用關係數據庫,對此你怎麼看?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章