Sql Or NoSql,看完這一篇你就懂了

五月的倉頡發表於2019-08-12

前言

你是否在為系統的資料庫來一波大流量就幾乎打滿CPU,日常CPU居高不下煩惱?你是否在各種NoSql間糾結不定,到底該選用那種最好?今天的你就是昨天的我,這也是寫這篇文章的初衷。

這篇文章是我好幾個月來一直想寫的一篇文章,也是一直想學習的一個內容,作為網際網路從業人員,我們要知道關係型資料庫(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(代表----ElasticSearch)

傳統關係型資料庫主要通過索引來達到快速查詢的目的,但是在全文搜尋的場景下,索引是無能為力的,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是基於列式儲存的,那麼什麼是列式儲存呢,列式NoSql和關係型資料庫一樣都有主鍵的概念,區別在於關係型資料庫是按照行組織的資料:

看到每行有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的關係型資料庫就完事了,它的優缺點比較一目瞭然,優點:

  • 沒有預定義的欄位,擴充套件欄位容易
  • 相較於關係型資料庫,讀寫效能優越,命中二級索引的查詢不會比關係型資料庫慢,對於非索引欄位的查詢則是全面勝出

缺點在於:

  • 不支援事務操作,雖然Mongodb4.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這方面的人才,運維上可能會有一定難度,出於實際情況考慮,可先用關係型資料庫頂一陣子

所以,如果不考慮實際情況,雖然合適有些儲存引擎更加合適,但是強行使用反而適得其反,總而言之,適合自己的才是最好的。

 

相關文章