到底選擇SQL還是NoSQL?看這里!
你是否在為系統的數據庫來一波大流量就幾乎打滿 CPU,日常 CPU 居高不下煩惱?你是否在各種 NoSQL 間糾結不定,到底該選用哪種最好?今天的你就是昨天的我,這也是我寫這篇文章的初衷。
圖片來自 Pexels
作為互聯網從業人員,我們要知道關系型數據庫(MySQL、Oracle)無法滿足我們對存儲的所有要求,因此對底層存儲的選型,對每種存儲引擎的理解非常重要。
同時也由于過去一段時間的工作經歷,對這塊有了一些更多的思考,想通過自己的總結把這塊寫出來分享給大家。
結構化數據、非結構化數據與半結構化數據
文章的開始,聊一下結構化數據、非結構化數據與半結構化數據,因為數據特點的不同,將在技術上直接影響存儲引擎的選型。
首先是結構化數據,根據定義結構化數據指的是由二維表結構來邏輯表達和實現的數據,嚴格遵循數據格式與長度規范,也稱作為行數據,特點為:數據以行為單位,一行數據表示一個實體的信息,每一行數據的屬性是相同的。
例如:
因此關系型數據庫很好契合結構化數據的特點,關系型數據庫也是關系型數據最主要的存儲與管理引擎。
非結構化數據,指的是數據結構不規則或不完整,沒有任何預定義的數據模型,不方便用二維邏輯表來表現的數據,例如辦公文檔(Word)、文本、圖片、HTML、各類報表、視頻音頻等。
介于結構化與非結構化數據之間的數據就是半結構化數據了,它是結構化數據的一種形式,雖然不符合二維邏輯這種數據模型結構,但是包含相關標記,用來分割語義元素以及對記錄和字段進行分層。
常見的半結構化數據有 XML 和 JSON,例如:
- <person>
- <name>張三</name>
- <age>18</age>
- <phone>12345</phone>
- </person>
這種結構也被成為自描述的結構。
以關系型數據庫的方式做存儲的架構演進
首先,我們看一下使用關系型數據庫的方式,企業一個系統發展的幾個階段的架構演進(由于本文寫的是 SQL 與 NoSQL,因此只以存儲方式作為切入點,不會涉及類似 MQ、ZK 這些中間件內容):
階段一
企業剛發展的階段,最簡單,一個應用服務器配一個關系型數據庫,每次讀寫數據庫。
階段二
無論是使用 MySQL 還是 Oracle 還是別的關系型數據庫,數據庫通常不會先成為性能瓶頸,通常隨著企業規模的擴大,一臺應用服務器扛不住上游過來的流量且一臺應用服務器會產生單點故障的問題。
因此加應用服務器并且在流量入口使用 Nginx 做一層負載均衡,保證把流量均勻打到應用服務器上。
階段三
隨著企業規模的繼續擴大,此時由于讀寫都在同一個數據庫上,數據庫性能出現一定的瓶頸。
此時簡單地做一層讀寫分離,每次寫主庫,讀備庫,主備庫之間通過 Binlog 同步數據,就能很大程度上解決這個階段的數據庫性能問題。
階段四
企業發展越來越好了,業務越來越大了,做了讀寫分離數據庫壓力還是越來越大,這時候怎么辦呢?
一臺數據庫扛不住,那我們就分幾臺吧,做分庫分表,對表做垂直拆分,對庫做水平拆分。
以擴數據庫為例,擴出兩臺數據庫,以一定的單號(例如交易單號),以一定的規則(例如取模)。
交易單號對 2 取模為 0 的丟到數據庫 1 去,交易單號對 2 取模為 1 的丟到數據庫 2 去,通過這樣的方式將寫數據庫的流量均分到兩臺數據庫上。
一般分庫分表會使用 Shard 的方式,通過一個中間件,便于連接管理、數據監控且客戶端無需感知數據庫 IP。
關系型數據庫的優點
上面的方式,看似可以解決問題(實際上確實也能解決很多問題),正常對關系型數據庫做一下讀寫分離+分庫分表,支撐個 1W+ 的讀寫 QPS 還是問題不大的。
但是受限于關系型數據庫本身,這套架構方案依然有著明顯的不足,下面對利用關系型數據庫方式做存儲的方案的優點先進行一下分析,后一部分再分析一下缺點,對某個技術的優缺點的充分理解是技術選型的前提。
①易理解
因為行+列的二維表邏輯是非常貼近邏輯世界的一個概念,關系模型相對網狀、層次等其他模型更加容易被理解。
②操作方便
通用的 SQL 語言使得操作關系型數據庫非常方便,支持 Join 等復雜查詢。
③數據一致性
支持 ACID 特性,可以維護數據之間的一致性,這是使用數據庫非常重要的一個理由之一。
例如同銀行轉賬,張三轉給李四 100 元錢,張三扣 100 元,李四加 100 元,而且必須同時成功或者同時失敗,否則就會造成用戶的資損。
④數據穩定
數據持久化到磁盤,沒有丟失數據風險,支持海量數據存儲。
⑤服務穩定
最常用的關系型數據庫產品 MySQL、Oracle 服務器性能卓越,服務穩定,通常很少出現宕機異常。
關系型數據庫的缺點
緊接著的,我們看一下關系型數據庫的缺點,也是比較明顯的。
①高并發下 IO 壓力大
數據按行存儲,即使只針對其中某一列進行運算,也會將整行數據從存儲設備中讀入內存,導致 IO 較高。
②為維護索引付出的代價大
為了提供豐富的查詢能力,通常熱點表都會有多個二級索引,一旦有了二級索引,數據的新增必然伴隨著所有二級索引的新增。
數據的更新也必然伴隨著所有二級索引的更新,這不可避免地降低了關系型數據庫的讀寫能力,且索引越多讀寫能力越差。
有機會的話可以看一下自己公司的數據庫,除了數據文件不可避免地占空間外,索引占的空間其實也并不少。
③為維護數據一致性付出的代價大
數據一致性是關系型數據庫的核心,但是同樣為了維護數據一致性的代價也是非常大的。
我們都知道 SQL 標準為事務定義了不同的隔離級別,從低到高依次是讀未提交、讀已提交、可重復讀、串行化,事務隔離級別月底,可能出現的并發異常越多,但是通常而言能提供的并發能力越強。
那么為了保證事務一致性,數據庫就需要提供并發控制與故障恢復兩種技術,前者用于減少并發異常,后者可以在系統異常的時候保證事務與數據庫狀態不會被破壞。
對于并發控制,其核心思想就是加鎖,無論是樂觀鎖還是悲觀鎖,只要提供的隔離級別越高,那么讀寫性能必然越差。
④水平擴展后帶來的種種問題難處理
前文提過,隨著企業規模擴大,一種方式是對數據庫做分庫,做了分庫之后,數據遷移(1 個庫的數據按照一定規則打到 2 個庫中)、跨庫 Join(訂單數據里有用戶數據,兩條數據不在同一個庫中)、分布式事務處理都是需要考慮的問題,尤其是分布式事務處理,業界當前都沒有特別好的解決方案。
⑤表結構擴展不方便
由于數據庫存儲的是結構化數據,因此表結構 Schema 是固定的,擴展不方便,如果需要修改表結構,需要執行 DDL(data definition language)語句修改,修改期間會導致鎖表,部分服務不可用。
⑥全文搜索功能弱
例如 like "%中國真偉大%",只能搜索到"2019年中國真偉大,愛祖國",無法搜索到"中國真是太偉大了"這樣的文本,即不具備分詞能力。
且 like 查詢在"%中國真偉大"這樣的搜索條件下,無法命中索引,將會導致查詢效率大大降低。
寫了這么多,我的理解核心還是前三點,它反映出的一個問題是關系型數據庫在高并發下的能力是有瓶頸的。
尤其是寫入/更新頻繁的情況下,出現瓶頸的結果就是數據庫 CPU 高、SQL 執行慢、客戶端報數據庫連接池不夠等錯誤,因此例如萬人秒殺這種場景,我們絕對不可能通過數據庫直接去扣減庫存。
可能有朋友說,數據庫在高并發下的能力有瓶頸,我公司有錢,加 CPU、換固態硬盤、繼續買服務器加數據庫做分庫不就好了。
問題是這是一種性價比非常低的方式,花 1000 萬達到的效果,換其他方式可能 100 萬就達到了,不考慮人員、服務器投入產出比的 Leader 就是個不合格的 Leader。
且關系型數據庫的方式,受限于它本身的特點,可能花了錢都未必能達到想要的效果。
至于什么是花 100 萬就能達到花 1000 萬效果的方式呢?可以繼續往下看,這就是我們要說的 NoSQL。
結合 NoSQL 的方式做存儲的架構演進
像上文分析的,數據庫作為一種關系型數據的存儲引擎,存儲的是關系型數據,它有優點,同時也有明顯的缺點。
因此通常在企業規模不斷擴大的情況下,不會一味指望通過增強數據庫的能力來解決數據存儲問題,而是會引入其他存儲,也就是我們說的 NoSQL。
NoSQL 的全稱為 Not Only SQL,泛指非關系型數據庫,是對關系型數據庫的一種補充。
特別要注意補充這兩個字,這意味著 NoSQL 與關系型數據庫并不是對立關系,二者各有優劣,取長補短,在合適的場景下選擇合適的存儲引擎才是正確的做法。
比較簡單的 NoSQL 就是緩存:
針對那些讀遠多于寫的數據,引入一層緩存,每次讀從緩存中讀取,緩存中讀取不到,再去數據庫中取,取完之后再寫入到緩存,對數據做好失效機制通常就沒有大問題了。
通常來說,緩存是性能優化的第一選擇也是見效最明顯的方案。但是,緩存通常都是 KV 型存儲且容量有限(基于內存),無法解決所有問題,于是再進一步的優化,我們繼續引入其他 NoSQL:
數據庫、緩存與其他 NoSQL 并行工作,充分發揮每種 NoSQL 的特點。當然 NoSQL 在性能方面大大優于關系型數據庫的同時,往往也伴隨著一些特性的缺失,比較常見的就是事務功能的缺失。
下面看一下常用的 NoSQL 及他們的代表產品,并對每種 NoSQL 的優缺點和適用場景做一下分析,便于熟悉每種 NoSQL 的特點,方便技術選型。
KV 型 NoSQL(代表:Redis)
KV 型 NoSQL 顧名思義就是以鍵值對形式存儲的非關系型數據庫,是最簡單、最容易理解也是大家最熟悉的一種 NoSQL,因此比較快地帶過。
Redis、MemCache 是其中的代表,Redis 又是 KV 型 NoSQL 中應用很廣泛的 NoSQL。
KV 型數據庫以 Redis 為例,最大的優點我總結下來就兩點:
- 數據基于內存,讀寫效率高。
- KV 型數據,時間復雜度為 O(1),查詢速度快。
因此,KV 型 NoSQL 最大的優點就是高性能,利用 Redis 自帶的 BenchMark 做基準測試,TPS 可達到 10 萬的級別,性能非常強勁。
同樣的 Redis 也有所有 KV 型 NoSQL 都有的比較明顯的缺點:
- 只能根據 K 查 V,無法根據 V 查 K。
- 查詢方式單一,只有 KV 的方式,不支持條件查詢,多條件查詢唯一的做法就是數據冗余,但這會極大的浪費存儲空間。
- 內存是有限的,無法支持海量數據存儲。
- 同樣的,由于 KV 型 NoSQL 的存儲是基于內存的,會有丟失數據的風險。
綜上所述,KV 型 NoSQL 最合適的場景就是緩存的場景:
- 讀遠多于寫。
- 讀取能力強。
- 沒有持久化的需求,可以容忍數據丟失,反正丟了再查詢一把寫入就是了。
例如根據用戶 id 查詢用戶信息,每次根據用戶 id 去緩存中查詢一把,查到數據直接返回,查不到去關系型數據庫里面根據 id 查詢一把數據寫到緩存中去。
搜索型NoSQL(代表:ES)
傳統關系型數據庫主要通過索引來達到快速查詢的目的,但是在全文搜索的場景下,索引是無能為力的。
like 查詢一來無法滿足所有模糊匹配需求,二來使用限制太大且使用不當容易造成慢查詢。
搜索型 NoSQL 的誕生正是為了解決關系型數據庫全文搜索能力較弱的問題,ElasticSearch 是搜索型 NoSQL 的代表產品。
全文搜索的原理是倒排索引,我們看一下什么是倒排索引。要說倒排索引我們先看下什么是正排索引,傳統的正排索引是文檔-->關鍵字的映射。
例如"Tom is my friend"這句話,會將其切分為"Tom"、"is"、"my"、"friend"四個單詞,在搜索的時候對文檔進行掃描,符合條件的查出來。
這種方式原理非常簡單,但是由于其檢索效率太低,基本沒什么實用價值。
倒排索引則完全相反,它是關鍵字-->文檔的映射,我用張表格展示一下就比較清楚了:
意思是我現在這里有"Tom is Tom"、"Tom is my friend"、"Thank you, Betty"、"Tom is Betty's husband"四句話。
搜索引擎會根據一定的切分規則將這句話切成 N 個關鍵字,并以關鍵字的維度維護關鍵字在每個文本中的出現次數。
這樣下次搜索"Tom"的時候,由于 Tom 這個詞語在"Tom is Tom"、"Tom is my friend"、"Tom is Betty's husband"三句話中都有出現,因此這三條記錄都會被檢索出來。
且由于"Tom is Tom"這句話中"Tom"出現了 2 次,因此這條記錄對"Tom"這個單詞的匹配度最高,最先展示。
這就是搜索引擎倒排索引的基本原理,假設某個關鍵字在某個文檔中出現,那么倒排索引中有兩部分內容:
- 文檔 id。
- 在該文檔中出現的位置情況。
可以舉一反三,我們搜索"Betty Tom"這兩個詞語也是一樣,搜索引擎將"Betty Tom"切分為"Tom"、"Betty"兩個單詞,根據開發者指定的滿足率,比如滿足率=50%,那么只要記錄中出現了兩個單詞之一的記錄都會被檢索出來,再按照匹配度進行展示。
搜索型 NoSQL 以 ElasticSearch 為例,它的優點為:
- 支持分詞場景、全文搜索,這是區別于關系型數據庫的最大特點。
- 支持條件查詢,支持聚合操作,類似關系型數據庫的 Group By,但是功能更加強大,適合做數據分析。
- 數據寫文件無丟失風險,在集群環境下可以方便橫向擴展,可承載 PB 級別的數據。
- 高可用,自動發現新的或者失敗的節點,重組和重新平衡數據,確保數據是安全和可訪問的。
同樣,ElasticSearch 也有比較明顯的缺點:
①性能全靠內存來頂,也是使用的時候最需要注意的點,非常吃硬件資源、吃內存,大數據量下 64G+SSD 基本是標配,算得上是數據庫中的愛馬仕了。
為什么要專門提一下內存呢,因為內存這個東西是很值錢的,相同的配置多一倍內存,一個月差不多就要多花幾百塊錢。
至于 ElasticSearch 內存用在什么地方,大概有如下這些:
- Indexing Buffer:ElasticSearch 基于 Luence,Lucene 的倒排索引是先在內存里生成,然后定期以 Segment File 的方式刷磁盤的,每個 Segment File 實際就是一個完整的倒排索引。
- Segment Memory:倒排索引前面說過是基于關鍵字的,Lucene 在 4.0 后會將所有關鍵字以 FST 這種數據結構的方式將所有關鍵字在啟動的時候全量加載到內存,加快查詢速度,官方建議至少留系統一半內存給 Lucene。
- 各類緩存:Filter Cache、Field Cache、Indexing Cache 等,用于提升查詢分析性能,例如 Filter Cache 用于緩存使用過的 Filter 的結果集。
- Cluter State Buffer:ElasticSearch 被設計為每個 Node 都可以響應用戶請求,因此每個 Node 的內存中都包含有一份集群狀態的拷貝,一個規模很大的集群這個狀態信息可能會非常大。
②讀寫之間有延遲,寫入的數據差不多 1s 樣子會被讀取到,這也正常,寫入的時候自動加入這么多索引肯定影響性能。
③數據結構靈活性不高,ElasticSearch 這個東西,字段一旦建立就沒法修改類型了,假如建立的數據表某個字段沒有加全文索引,想加上,那么只能把整個表刪了再重建。
因此,搜索型 NoSQL 最適用的場景就是有條件搜索尤其是全文搜索的場景,作為關系型數據庫的一種替代方案。
另外,搜索型數據庫還有一種特別重要的應用場景。我們可以想,一旦對數據庫做了分庫分表后,原來可以在單表中做的聚合操作、統計操作是否統統失效?
例如我把訂單表分 16 個庫,1024 張表,那么訂單數據就散落在 1024 張表中,我想要統計昨天浙江省單筆成交金額最高的訂單是哪筆如何做?我想要把昨天的所有訂單按照時間排序分頁展示如何做?
這就是文檔型 NoSQL 的另一大作用了,我們可以把分表之后的數據統一打在文檔型 NoSQL 中,利用文檔型 NoSQL 的搜索與聚合能力完成對全量數據的查詢。
至于為什么把它放在 KV 型 NoSQL 后面作為第二個寫呢,因為通常搜索型 NoSQL 也會作為一層前置緩存,來對關系型數據庫進行保護。
列式 NoSQL(代表:HBase)
列式 NoSQL,大數據時代代表性的技術之一了,以 HBase 為代表。列式 NoSQL 是基于列式存儲的,那么什么是列式存儲呢,列式 SQL 和關系型數據庫一樣都有主鍵的概念,區別在于關系型數據庫是按照行組織的數據:
看到每行有 name、phone、address 三個字段,這是行式存儲的方式,且可以觀察 id=2 的這條數據,即使 phone 字段沒有,它也是占空間的。
列式存儲完全是另一種方式,它是按每一列進行組織的數據:
這么做有什么好處呢?大致有以下幾點:
- 查詢時只有指定的列會被讀取,不會讀取所有列。
- 存儲上節約空間,Null 值不會被存儲,一列中有時候會有很多重復數據(尤其是枚舉數據,性別、狀態等),這類數據可壓縮,行式數據庫壓縮率通常在 3:1~5:1 之間,列式數據庫的壓縮率一般在 8:1~30:1 左右。
- 列數據被組織到一起,一次磁盤 IO 可以將一列數據一次性讀取到內存中。
第二點說到了數據壓縮,什么意思呢,以比較常見的字典表壓縮方式舉例:

仔細看圖理解一下,應該就懂了。接著繼續講講優缺點,列式 NoSQL,以 HBase 為代表的,優點為:
- 海量數據無限存儲,PB 級別數據隨便存,底層基于 HDFS(Hadoop 文件系統),數據持久化。
- 讀寫性能好,只要沒有濫用造成數據熱點,讀寫基本隨便玩。
- 橫向擴展在關系型數據庫及非關系型數據庫中都是方便的之一,只需要添加新機器就可以實現數據容量的線性增長,且可用在廉價服務器上,節省成本。
- 本身沒有單點故障,可用性高。
- 可存儲結構化或者半結構化的數據。
- 列數理論上無限,HBase 本身只對列族數量有要求,建議 1~3 個。
說了這么多 HBase 的優點,又到了說 HBase 缺點的時候了:
- HBase 是 Hadoop 生態的一部分,因此它本身是一款比較重的產品,依賴很多 Hadoop 組件,數據規模不大沒必要用,運維還是有點復雜的。
- KV 式,不支持條件查詢,或者說條件查詢非常非常弱吧,HBase 在 Scan 掃描一批數據的情況下還是提供了前綴匹配這種 API 的,條件查詢除非定義多個 RowKey 做數據冗余。
- 不支持分頁查詢,因為統計不了數據總數。
因此 HBase 比較適用于那種 KV 型的且未來無法預估數據增長量的場景,另外 HBase 使用還是需要一定的經驗,主要體現在 RowKey 的設計上。
文檔型 NoSQL(代表:MongoDB)
坦白講,根據我的工作經歷,文檔型 NoSQL 我只有比較淺的使用經驗,因此這部分只能結合之前的使用與網上的文章大致給大家介紹一下。
什么是文檔型 NoSQL 呢,文檔型 NoSQL 指的是將半結構化數據存儲為文檔的一種 NoSQL。
文檔型 NoSQL 通常以 JSON 或者 XML 格式存儲數據,因此文檔型 NoSQL 是沒有 Schema 的。
由于沒有 Schema 的特性,我們可以隨意地存儲與讀取數據,因此文檔型 NoSQL 的出現是解決關系型數據庫表結構擴展不方便的問題的。
MongoDB 是文檔型 NoSQL 的代表產品,同時也是所有 NoSQL 產品中的明星產品之一,因此這里以 MongoDB 為例。
按我的理解,作為文檔型 NoSQL,MongoDB 是一款完全和關系型數據庫對標的產品,就我們從存儲上來看:
可看到,關系型數據庫是按部就班地每個字段一列存,在 MongDB 里面就是一個 JSON 字符串存儲。
關系型數據可以為 name、phone 建立索引,MongoDB 使用 createIndex 命令一樣可以為列建立索引,建立索引之后可以大大提升查詢效率。
其他方面而言,就大的基本概念,二者之間基本也是類似的:
因此,對于 MongDB,我們只要理解成一個 Free-Schema 的關系型數據庫就完事了,它的優缺點比較一目了然,優點:
- 沒有預定義的字段,擴展字段容易。
- 相較于關系型數據庫,讀寫性能優越,命中二級索引的查詢不會比關系型數據庫慢,對于非索引字段的查詢則是全面勝出。
缺點在于:
- 不支持事務操作,雖然 MongoDB 4.0 之后宣稱支持事務,但是效果待觀測。
- 多表之間的關聯查詢不支持(雖然有嵌入文檔的方式),Join 查詢還是需要多次操作。
- 空間占用較大,這個是 MongDB 的設計問題,空間預分配機制+刪除數據后空間不釋放,只有用 db.repairDatabase() 去修復才能釋放。
- 目前沒發現 MongoDB 有關系型數據庫例如 MySQL 的 Navicat 這種成熟的運維工具。
總而言之,MongDB 的使用場景很大程度上可以對標關系型數據庫,但是比較適合處理那些沒有 Join、沒有強一致性要求且表 Schema 會常變化的數據。
總結:數據庫與 NoSQL 及各種 NoSQL 間的對比
最后一部分,做一個總結,本文歸根到底是兩個話題:
- 何時選用關系型數據庫,何時選用非關系型數據庫。
- 選用非關系型數據庫,使用哪種非關系型數據庫。
首先是第一個話題,關系型數據庫與非關系型數據庫的選擇,在我理解里面無非就是兩點考慮:
第一點,不多解釋應該都理解,非關系型數據庫都是通過犧牲了 ACID 特性來獲取更高的性能的,假設兩張表之間有比較強的一致性需求,那么這類數據是不適合放在非關系型數據庫中的。
第二點,核心數據不走非關系型數據庫,例如用戶表、訂單表,但是這有一個前提,就是這一類核心數據會有多種查詢模式。
例如用戶表有 ABCD 四個字段,可能根據 AB 查,可能根據 AC 查,可能根據 D 查,假設核心數據,但是就是個 KV 形式,比如用戶的聊天記錄,那么 HBase 一存就完事了。
從這幾年的工作經驗來看,非核心數據尤其是日志、流水一類中間數據千萬不要寫在關系型數據庫中,這一類數據通常有兩個特點:
- 寫遠高于讀
- 寫入量巨大
一旦使用關系型數據庫作為存儲引擎,將大大降低關系型數據庫的能力,正常讀寫 QPS 不高的核心服務會受這一類數據讀寫的拖累。
接著是第二個問題,如果我們使用非關系型數據庫作為存儲引擎,那么如何選型?
其實上面的文章基本都寫了,這里只是做一個總結(所有的缺點都不會體現事務這個點,因為這是所有 NoSQL 相比關系型數據庫共有的一個問題):
但是這里特別說明,選型一定要結合實際情況而不是照本宣科,比如:
- 企業發展之初,明明一個關系型數據庫就能搞定且支撐一年的架構,搞一套大而全的技術方案出來。
- 有一些數據條件查詢多,更適合使用 ElasticSearch 做存儲降低關系型數據庫壓力,但是公司成本有限,這種情況下這類數據可以嘗試繼續使用關系型數據庫做存儲。
- 有一類數據格式簡單,就是這個 KV 類型且增長量大,但是公司沒有 HBase 這方面的人才,運維上可能會有一定難度,出于實際情況考慮,可先用關系型數據庫頂一陣子。
所以,如果不考慮實際情況,雖然確實有些存儲引擎更加合適,但是強行使用反而適得其反,總而言之,適合自己的才是最好的。