後端思維之數據庫性能優化方案

前言

  毫不誇張的說咱們後端工程師,無論在哪家公司,呆在哪個團隊,做哪個系統,遇到的第一個讓人頭疼的問題絕對是數據庫性能問題如果我們有一套成熟的方法論,能讓大家快速、準確的去選擇出合適的優化方案,我相信能夠快速準備解決咱麼日常遇到的80%甚至90%的性能問題。

  從解決問題的角度出發,我們得先了解到問題的原因;其次我們得有一套思考、判斷問題的流程方式,讓我們合理的站在哪個層面選擇方案;最後從衆多的方案裏面選擇一個適合的方案進行解決問題,找到一個合適的方案的前提是我們自己對各種方案之間的優缺點、場景有足夠的瞭解,沒有一個方案是完全可以通喫通用的,軟件工程沒有銀彈。

  下文的我工作多年以來,曾經使用過的八大方案,結合了平常自己學習收集的一些資料,以系統、全面的方式整理成了這篇博文,也希望能讓一些有需要的同行在工作上、成長上提供一定的幫助。

爲什麼數據庫會慢?

慢的本質

查找的時間複雜度

查找算法

存儲數據結構

數據總量

數據拆分

高負載

CPU、磁盤繁忙

無論是關係型數據庫還是NoSQL,任何存儲系統決定於其查詢性能的主要有三種:

  • 查找的時間複雜度
  • 數據總量
  • 高負載

而決定於查找時間複雜度主要有兩個因素:

  • 查找算法
  • 存儲數據結構

無論是哪種存儲,數據量越少,自然查詢性能就越高,隨着數據量增多,資源的消耗(CPU、磁盤讀寫繁忙)、耗時也會越來越高。

從關係型數據庫角度出發,索引結構基本固定是B+Tree,時間複雜度是O(log n),存儲結構是行式存儲。因此咱們對於關係數據庫能優化的一般只有數據量。

而高負載造成原因有高併發請求、複雜查詢等,導致CPU、磁盤繁忙等,而服務器資源不足則會導致慢查詢等問題。該類型問題一般會選擇集羣、數據冗餘的方式分擔壓力。

應該站在哪個層面思考優化?

  從上圖可見,自頂向下的一共有四層,分別是硬件、存儲系統、存儲結構、具體實現。層與層之間是緊密聯繫的,每一層的上層是該層的載體;因此越往頂層越能決定性能的上限,同時優化的成本也相對會比較高,性價比也隨之越低。以最底層的具體實現爲例,那麼索引的優化的成本應該是最小的,可以說加了索引後無論是CPU消耗還是響應時間都是立竿見影降低;然而一個簡單的語句,無論如何優化加索引也是有侷限的,當在具體實現這層沒有任何優化空間的時候就得往上一層【存儲結構】思考,思考是否從物理表設計的層面出發優化(如分庫分表、壓縮數據量等),如果是文檔型數據庫得思考下文檔聚合的結果;如果在存儲結構這層優化得沒效果,得繼續往再上一次進行考慮,是否關係型數據庫應該不適合用在現在得業務場景?如果要換存儲,那麼得換怎樣得NoSQL?

  所以咱們優化的思路,出於性價比的優先考慮具體實現,實在沒有優化空間了再往上一層考慮。當然如果公司有錢,直接使用鈔能力,繞過了前面三層,這也是一種便捷的應急處理方式。

  該篇文章不討論頂與底的兩個層面的優化,主要從存儲結構、存儲系統中間兩層的角度出發進行探討

八大方案總結

方案總覽

方案類型

方案描述

數據類型

收益類型

應對場景

減少數據量

數據序列化存儲

靜態數據

短期收益

大數據量

數據歸檔

動態數據

中期收益

大數據量

中間表生成

靜態數據

長期收益

大數據量、高負載

分庫分表

動態數據

長期收益

大數據量、高負載

用空間換性能

分佈式緩存

靜態數據

短期收益

高負載

一主多從

動態數據

中期收益

高負載

選擇合適的存儲系統

CQRS

動態數據

長期收益

大數據量、高負載

更換存儲系統

動態數據

長期收益

大數據量、高負載

  數據庫的優化方案核心本質有三種:減少數據量用空間換性能選擇合適的存儲系統,這也對應了開篇講解的慢的三個原因:數據總量、高負載、查找的時間複雜度。

  這裏大概解釋下收益類型:短期收益,處理成本低,能緊急應對,久了則會有技術債務;長期收益則跟短期收益相反,短期內處理成本高,但是效果能長久使用,擴展性會更好。

     靜態數據意思是,相對改動頻率比較低的,也無需過多聯表的,where過濾比較少。動態數據與之相反,更新頻率高,通過動態條件篩選過濾。

減少數據量

減少數據量類型共有四種方案:數據序列化存儲、數據歸檔、中間表生成、分庫分表。

就如上面所說的,無論是哪種存儲,數據量越少,自然查詢性能就越高,隨着數據量增多,資源的消耗(CPU、磁盤讀寫繁忙)、耗時也會越來越高。目前市面上的NoSQL基本上都支持分片存儲,所以其天然分佈式寫的能力從數據量上能得到非常的解決方案。而關係型數據庫,查找算法與存儲結構是可以優化的空間比較少,因此咱們一般思考出發點只有從如何減少數據量的這個角度進行選擇優化,因此本類型的優化方案主要針對關係型數據庫進行處理。

數據歸檔

數據歸檔

做法

場景

優點

缺點

利用數據庫作業,定時把歷史數據移到歷史表或者庫

局部的熱點數據

結構無需改動,少侵入性

熱點數據過多仍會導致性能問題


  注意點:別一次性遷移數量過多,建議低頻率多次限量遷移。像MySQL由於刪除數據後是不會釋放空間的,可以執行命令OPTIMIZE TABLE釋放存儲空間,但是會鎖表,如果存儲空間還滿足,可以不執行。

  建議優先考慮該方案,主要通過數據庫作業把非熱點數據遷移到歷史表,如果需要查歷史數據,可新增業務入口路由到對應的歷史表(庫)。

       在數據庫以序列化存儲的方式,對於一些不需要結構化存儲的業務來說是一種很好減少數據量的方式,特別是對於一些M*N的數據量的業務場景,如果以M作爲主表優化,那麼就可以把數據量維持最多是M的量級。另外像訂單的地址信息,這種業務一般是不需要根據裏面的字段檢索出來,也比較適合。

       這種方案我認爲屬於一種臨時性的優化方案,無論是從序列化後丟失了部份字段的查詢能力,還是這方案的可優化性都是有限的。

中間表(結果表)

中間表(結果表)

做法

場景

優點

缺點

通過調度任務定時,把某個業務以多個維度進行聚合分組

報表型、排行榜等靜態數據

壓縮比率大

需要開發人員針對場景業務進行開發

  
  中間表(結果表)其實就是利用調度任務把複雜查詢的結果跑出來存儲到一張額外的物理表,因爲這張物理表存放的是通過跑批匯總後的數據,因此可以理解成根據原有的業務進行了高度的數據壓縮。以報表爲例,如果一個月的源數據有數十萬,我們通過調度任務以月的維度生成,那麼等於把原有的數據壓縮了幾十萬分之一;接下來的季報和年報可以根據月報*N來進行統計,以這種方式處理的數據,就算三年、五年甚至十年數據量都可以在接受範圍之內,而且可以精確計算得到。
  那麼數據的壓縮比率是否越低越好?下面有一段口訣:
  • 字段越多,粒度越細,靈活性越高,可以以中間表進行不同業務聯表處理。
  • 字段越少,粒度越粗,靈活性越低,一般作爲結果表查詢出來。

數據序列化存儲

數據序列化存儲

做法

場景

優點

缺點

把一對多的數據,通過序列化字符串存儲

不需要要求所有字段作爲結構化存儲

壓縮比率高

序列化的字段無法聯表

分庫分表

  分庫分表作爲數據庫優化的一種非常經典的優化方案,特別是在以前NoSQL還不是很成熟的年代,這個方案就如救命草一般的存在。

  如今也有不少同行也會選擇這種優化方式,但是從我角度來看,分庫分表是一種優化成本很大的方案。這裏我有幾個建議:

  1. 分庫分表是實在沒有辦法的辦法,應放到最後選擇。
  2. 優先選擇NoSQL代替,因爲NoSQL誕生基本上爲了擴展性與高性能。
  3. 究竟分庫還是分表?量大則分表,併發高則分庫
  4. 不考慮擴容,一部做到位。因爲技術更新太快了,每3-5年一大變。

拆分方式

                                            分庫分表-拆分方式

拆分方式

角度

優點

垂直拆分

按照業務拆分

降低業務耦合度

減少字段,物理頁所擁有的行數則變多

水平拆分

從物理層面分片

從根本上減少數據量

  只要涉及到這個拆,那麼無論是微服務也好,分庫分表也好,拆分的方式主要分兩種:垂直拆分、水平拆分

  垂直拆分更多是從業務角度進行拆分,主要是爲了降低業務耦合度;此外以SQL Server爲例,一頁是8KB存儲,如果在一張表裏字段越多,一行數據自然佔的空間就越大,那麼一頁數據所存儲的行數就自然越少,那麼每次查詢所需要IO則越高因此性能自然也越慢;因此反之,減少字段也能很好提高性能。之前我聽說某些同行的表有80個字段,幾百萬的數據就開始慢了。

  水平拆分更多是從技術角度進行拆分,拆分後每張表的結構是一模一樣的,簡而言之就是把原有一張表的數據,通過技術手段進行分片到多張表存儲,從根本上解決了數據量的問題。

路由方式

路由方式

算法

優點

缺點

區間範圍

查詢定位比較容易

容易造成數據不平均(熱點數據)

容易忘記創建新表

Hash

分片均勻

必須帶分區鍵,不帶分區鍵則會所有表都掃描一遍

分庫情況下無法使用關係型數據庫的特性(Join、聚合計算、分頁)

分片映射表

補充方案

二次查詢

  進行水平拆分後,根據分區鍵(sharding key)原來應該在同一張表的數據拆解寫到不同的物理表裏,那麼查詢也得根據分區鍵進行定位到對應的物理表從而把數據給查詢出來。

  路由方式一般有三種區間範圍、Hash、分片映射表,每種路由方式都有自己的優點和缺點,可以根據對應的業務場景進行選擇。

  區間範圍根據某個元素的區間的進行拆分,以時間爲例子,假如有個業務我們希望以月爲單位拆分那麼表就會拆分像 table_2022-04,這種對於文檔型、ElasticSearch這類型的NoSQL也適用,無論是定位查詢,還是日後清理維護都是非常的方便的。那麼缺點也明顯,會因爲業務獨特性導致數據不平均,甚至不同區間範圍之間的數據量差異很大。

  Hash也是一種常用的路由方式,根據Hash算法取模以數據量均勻分別存儲在物理表裏,缺點是對於帶分區鍵的查詢依賴特別強,如果不帶分區鍵就無法定位到具體的物理表導致相關所有表都查詢一次,而且在分庫的情況下對於Join、聚合計算、分頁等一些RDBMS的特性功能還無法使用。

  一般分區鍵就一個,假如有時候業務場景得用不是分區鍵的字段進行查詢,那麼難道就必須得全部掃描一遍?其實可以使用分片映射表的方式,簡單來說就是額外有一張表記錄額外字段與分區鍵的映射關係。舉個例子,有張訂單表,原本是以UserID作爲分區鍵拆分的,現在希望用OrderID進行查詢,那麼得有額外得一張物理表記錄了OrderID與UserID的映射關係。因此得先查詢一次映射表拿到分區鍵,再根據分區鍵的值路由到對應的物理表查詢出來。可能有些朋友會問,那這映射表是否多一個映射關係就多一張表,還是多個映射關係在同一張表。我優先建議單獨處理,如果說映射表字段過多,那跟不進行水平拆分時的狀態其實就是一致的,這又跑回去的老問題。

用空間換性能 

  該類型的兩個方案都是用來應對高負載的場景,方案有以下兩種:分佈式緩存、一主多從。

  與其說這個方案叫用空間換性能,我認爲用空間換資源更加貼切一些。因此兩個方案的本質主要通數據冗餘、集羣等方式分擔負載壓力。

  對於關係型數據庫而言,因爲他的ACID特性讓它天生不支持寫的分佈式存儲,但是它依然天然的支持分佈式讀。 

分佈式緩存

分佈式緩存

做法

場景

缺點

Cache Aside

應對高併發讀

動態條件比較多的業務場景,緩存命中低

僞靜態數據(業務配置、低時效的數據)

實時性要求高的數據場景,處理起來比較花功夫

  緩存層級可以分好幾種:客戶端緩存API服務本地緩存分佈式緩存,咱們這次只聊分佈式緩存。一般我們選擇分佈式緩存系統都會優先選擇NoSQL的鍵值型數據庫,例如Memcached、Redis,如今Redis的數據結構多樣性,高性能,易擴展性也逐漸佔據了分佈式緩存的主導地位。

  緩存策略也主要有很多種:Cache-AsideRead/Wirte-ThroughWrite-Back,咱們用得比較多的方式主要Cache-Aside,具體流程可看下圖:

 

 我相信大家對分佈式緩存相對都比較熟悉了,但是我在這裏還是有幾個注意點希望提醒一下大家:

避免濫用緩存

  緩存應該是按需使用,從28法則來看,80%的性能問題由主要的20%的功能引起。濫用緩存的後果會導致維護成本增大,而且有一些數據一致性的問題也不好定位。特別像一些動態條件的查詢或者分頁,key的組裝是多樣化的,量大又不好用keys指令去處理,當然我們可以用額外的一個key把記錄數據的key以集合方式存儲,刪除時候做兩次查詢,先查Key的集合,然後再遍歷Key集合把對應的內容刪除。這一頓操作下來無疑是非常廢功夫的,誰弄誰知道。

避免緩存擊穿

  當緩存沒有數據,就得跑去數據庫查詢出來,這就是緩存穿透。假如某個時間臨界點數據是空的例如周排行榜,穿透過去的無論查找多少次數據庫仍然是空,而且該查詢消耗CPU相對比較高,併發一進來因爲缺少了緩存層的對高併發的應對,這個時候就會因爲併發導致數據庫資源消耗過高,這就是緩存擊穿。數據庫資源消耗過高就會導致其他查詢超時等問題。

  該問題的解決方案也簡單,對於查詢到數據庫的空結果也緩存起來,但是給一個相對快過期的時間。有些同行可能又會問,這樣不就會造成了數據不一致了麼?一般有數據同步的方案像分佈式緩存、後續會說的一主多從、CQRS,只要存在數據同步這幾個字,那就意味着會存在數據一致性的問題,因此如果使用上述方案,對應的業務場景應允許容忍一定的數據不一致。

不是所有慢查詢都適用 

  一般來說,慢的查詢都意味着比較喫資源的(CPU、磁盤I/O)。舉個例子,假如某個查詢功能需要3秒時間,串行查詢的時候並沒什麼問題,我們繼續假設這功能每秒大概QPS爲100,那麼在第一次查詢結果返回之前,接下來的所有查詢都應該穿透到數據庫,也就意味着這幾秒時間有300個請求到數據庫,如果這個時候數據庫CPU達到了100%,那麼接下來的所有查詢都會超時,也就是無法有第一個查詢結果緩存起來,從而還是形成了緩存擊穿。

一主多從

一主多從

場景

優點

缺點

分擔數據庫讀壓力

應急調整方便,單以運維直接解決。

高硬件成本

還沒找到更好的降低數據庫負載的臨時方案

擴展性有限

  常用的分擔數據庫壓力還有一種常用做法,就是讀寫分離、一主多從。咱們都是知道關係型數據庫天生是不具備分佈式分片存儲的,也就是不支持分佈式寫,但是它天然的支持分佈式讀。一主多從是部署多臺從庫只讀實例,通過冗餘主庫的數據來分擔讀請求的壓力,路由算法可有代碼實現或者中間件解決,具體可以根據團隊的運維能力與代碼組件支持視情況選擇。

  一主多從在還沒找到根治方案前是一個非常好的應急解決方案,特別是在現在雲服務的年代,擴展從庫是一件非常方便的事情,而且一般情況只需要運維或者DBA解決就行,無需開發人員接入。當然這方案也有缺點,因爲數據無法分片,所以主從的數據量完全冗餘過去,也會導致高的硬件成本。從庫也有其上限,從庫過多了會主庫的多線程同步數據的壓力。

選擇合適的存儲系統

  NoSQL主要以下五種類型:鍵值型、文檔型、列型、圖型、搜素引擎,不同的存儲系統直接決定了查找算法存儲數據結構,也應對了需要解決的不同的業務場景。NoSQL的出現也解決了關係型數據庫之前面臨的難題(性能、高併發、擴展性等)。

       例如,ElasticSearch的查找算法是倒排索引,可以用來代替關係型數據庫的低性能、高消耗的Like搜索(全表掃描)。而Redis的Hash結構決定了時間複雜度爲O(1),還有它的內存存儲,結合分片集羣存儲方式以至於可以支撐數十萬QPS。

  因此本類型的方案主要有兩種:CQRS、替換(選擇)存儲,這兩種方案的最終本質基本是一樣的主要使用合適存儲來彌補關係型數據庫的缺點,只不過切換過渡的方式會有點不一樣。

CQRS

CQS(命令查詢分離)指同一個對象中作爲查詢或者命令的方法,每個方法或者返回的狀態,要麼改變狀態,但不能兩者兼備  

CQRS

場景

優點

缺點

需要保留關係型數據庫的使用,又要使用NoSQL的高性能與可擴展性

原應用改動範圍比較小,兼容舊業務,只需要替換讀的底層。

高硬件成本

允許非實時的數據場景

即保留了關係型數據庫的ACID特性,又使用NoSQL的可擴展性與高性能

數據同步

  講解CQRS前得了解CQS,有些小夥伴看了估計還沒不是很清晰,我這裏用通俗的話解釋:某個對象的數據訪問的方法裏,要麼只是查詢,要麼只是寫入(更新)。而CQRS(命令查詢職責分離)基於CQS的基礎上,用物理數據庫來寫入(更新),而用另外的存儲系統來查詢數據。因此我們在某些業務場景進行存儲架構設計時,可以通過關係型數據庫的ACID特性進行數據的更新與寫入,用NoSQL的高性能與擴展性進行數據的查詢處理,這樣的好處就是關係型數據庫和NoSQL的優點都可以兼得,同時對於某些業務不適於一刀切的替換存儲的也可以有一個平滑的過渡。

  從代碼實現角度來看,不同的存儲系統只是調用對應的接口API,因此CQRS的難點主要在於如何進行數據同步。

數據同步方式

CQRS實現方式

方式

實時性

方案類型

優點

缺點

CDC(變更數據捕獲)

無業務侵入,解決多業務入口

額外中間件

領域事件

可讀性高

需要在框架代碼層面處理

調度任務定時同步

同CDC

物理刪除無法識別,只能全量

   一般討論到數據同步的方式主要是分拉:

  推指的是由數據變更端通過直接或者間接的方式把數據變更的記錄發送到接收端,從而進行數據的一致性處理,這種主動的方式優點是實時性高。

  拉指的是接收端定時的輪詢數據庫檢查是否有數據需要進行同步,這種被動的方式從實現角度來看比推簡單,因爲推是需要數據變更端支持變更日誌的推送的。

  而推的方式又分兩種:CDC(變更數據捕獲)和領域事件。對於一些舊的項目來說,某些業務的數據入口非常多,無法完整清晰的梳理清楚,這個時候CDC就是一種非常好的方式,只要從最底層數據庫層面把變更記錄取到就可。

  對於已經服務化的項目來說領域事件是一種比較舒服的方式,因爲CDC是需要數據庫額外開啓功能或者部署額外的中間件,而領域事件則不需要,從代碼可讀性來看會更高,也比較開發人員的維護思維模式。

 

替換(選擇)存儲系統

  因爲從本質來看該模式與CQRS的核心本質是一樣的,主要是要對NoSQL的優缺點有一個全面認識,這樣才能在對應業務場景選擇與判斷出一個合適的存儲系統。這裏我像大家介紹一本書馬丁.福勒《NoSQL精粹》,這本書我重複看了好幾遍,也很好全面介紹各種NoSQL優缺點和使用場景。

  當然替換存儲的時候,我這裏也有個建議:加入一箇中間版本,該版本做好數據同步與業務開關,數據同步要保證全量與增加的處理,隨時可以重來,業務開關主要是爲了後續版本的更新做的一個臨時型的功能,主要避免後續版本更新不順利或者因爲版本更新時導致的數據不一致的情況出現。在跑了一段時間後,驗證了兩個不同的存儲系統數據是一致的後,接下來就可以把數據訪問層的底層調用替換了。如此一來就可以平滑的更新切換。

結束

  本文到這裏就把八大方案介紹完了,在這裏再次提醒一句,每個方案都有屬於它的應對場景,咱們只能根據業務場景選擇對應的解決方案,沒有通喫,沒有銀彈。

  這八個方案裏,大部分都存在數據同步的情況,只要存在數據同步,無論是一主多從、分佈式緩存、CQRS都好,都會有數據一致性的問題導致,因此這些方案更多適合一些只讀的業務場景。當然有些寫後既查的場景,可以通過過渡頁或者廣告頁通過用戶點擊關閉切換頁面的方式來緩解數據不一致性的情況。

  通過這篇文章我相信大家對數據庫設計優化有了一個全面的認識,如果有更加的建議可以在下方評論反饋給給我。

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