點(diǎn)擊關(guān)注公眾號,,Java干貨及時送達(dá)??來源:cnblogs.com/skychen1218/p/16059148.html
前言毫不夸張的說咱們后端工程師,,無論在哪家公司,,呆在哪個團(tuán)隊,,做哪個系統(tǒng),遇到的第一個讓人頭疼的問題絕對是數(shù)據(jù)庫性能問題,。如果我們有一套成熟的方法論,,能讓大家快速、準(zhǔn)確的去選擇出合適的優(yōu)化方案,,我相信能夠快速準(zhǔn)備解決咱么日常遇到的80%甚至90%的性能問題,。 從解決問題的角度出發(fā),我們得先了解到問題的原因,;其次我們得有一套思考,、判斷問題的流程方式,讓我們合理的站在哪個層面選擇方案;最后從眾多的方案里面選擇一個適合的方案進(jìn)行解決問題,,找到一個合適的方案的前提是我們自己對各種方案之間的優(yōu)缺點(diǎn),、場景有足夠的了解,沒有一個方案是完全可以通吃通用的,,軟件工程沒有銀彈,。 下文的我工作多年以來,曾經(jīng)使用過的八大方案,,結(jié)合了平常自己學(xué)習(xí)收集的一些資料,,以系統(tǒng)、全面的方式整理成了這篇博文,,也希望能讓一些有需要的同行在工作上,、成長上提供一定的幫助。 為什么數(shù)據(jù)庫會慢,?無論是關(guān)系型數(shù)據(jù)庫還是NoSQL,,任何存儲系統(tǒng)決定于其查詢性能的主要有三種:
而決定于查找時間復(fù)雜度主要有兩個因素:
無論是哪種存儲,數(shù)據(jù)量越少,,自然查詢性能就越高,,隨著數(shù)據(jù)量增多,資源的消耗(CPU,、磁盤讀寫繁忙),、耗時也會越來越高。 從關(guān)系型數(shù)據(jù)庫角度出發(fā),,索引結(jié)構(gòu)基本固定是B+Tree,,時間復(fù)雜度是O(log n),存儲結(jié)構(gòu)是行式存儲,。因此咱們對于關(guān)系數(shù)據(jù)庫能優(yōu)化的一般只有數(shù)據(jù)量,。 而高負(fù)載造成原因有高并發(fā)請求、復(fù)雜查詢等,,導(dǎo)致CPU,、磁盤繁忙等,而服務(wù)器資源不足則會導(dǎo)致慢查詢等問題,。該類型問題一般會選擇集群,、數(shù)據(jù)冗余的方式分擔(dān)壓力。 應(yīng)該站在哪個層面思考優(yōu)化,?從上圖可見,,自頂向下的一共有四層,分別是硬件,、存儲系統(tǒng),、存儲結(jié)構(gòu),、具體實(shí)現(xiàn)。層與層之間是緊密聯(lián)系的,,每一層的上層是該層的載體,;因此越往頂層越能決定性能的上限,同時優(yōu)化的成本也相對會比較高,,性價比也隨之越低,。以最底層的具體實(shí)現(xiàn)為例,那么索引的優(yōu)化的成本應(yīng)該是最小的,,可以說加了索引后無論是CPU消耗還是響應(yīng)時間都是立竿見影降低,; 然而一個簡單的語句,無論如何優(yōu)化加索引也是有局限的,,當(dāng)在具體實(shí)現(xiàn)這層沒有任何優(yōu)化空間的時候就得往上一層【存儲結(jié)構(gòu)】思考,,思考是否從物理表設(shè)計的層面出發(fā)優(yōu)化(如分庫分表、壓縮數(shù)據(jù)量等),,如果是文檔型數(shù)據(jù)庫得思考下文檔聚合的結(jié)果,;如果在存儲結(jié)構(gòu)這層優(yōu)化得沒效果,得繼續(xù)往再上一次進(jìn)行考慮,,是否關(guān)系型數(shù)據(jù)庫應(yīng)該不適合用在現(xiàn)在得業(yè)務(wù)場景,?如果要換存儲,那么得換怎樣得NoSQL? 所以咱們優(yōu)化的思路,,出于性價比的優(yōu)先考慮具體實(shí)現(xiàn),,實(shí)在沒有優(yōu)化空間了再往上一層考慮。當(dāng)然如果公司有錢,,直接使用鈔能力,,繞過了前面三層,這也是一種便捷的應(yīng)急處理方式,。
八大方案總結(jié)數(shù)據(jù)庫的優(yōu)化方案核心本質(zhì)有三種: 減少數(shù)據(jù)量,、用空間換性能、選擇合適的存儲系統(tǒng),,這也對應(yīng)了開篇講解的慢的三個原因:數(shù)據(jù)總量,、高負(fù)載、查找的時間復(fù)雜度,。 這里大概解釋下收益類型: 短期收益,處理成本低,,能緊急應(yīng)對,,久了則會有技術(shù)債務(wù),;長期收益則跟短期收益相反,短期內(nèi)處理成本高,,但是效果能長久使用,,擴(kuò)展性會更好。 靜態(tài)數(shù)據(jù)意思是,,相對改動頻率比較低的,,也無需過多聯(lián)表的,where過濾比較少,。動態(tài)數(shù)據(jù)與之相反,,更新頻率高,通過動態(tài)條件篩選過濾,。 減少數(shù)據(jù)量減少數(shù)據(jù)量類型共有四種方案:數(shù)據(jù)序列化存儲,、數(shù)據(jù)歸檔、中間表生成,、分庫分表,。 就如上面所說的,無論是哪種存儲,,數(shù)據(jù)量越少,,自然查詢性能就越高,隨著數(shù)據(jù)量增多,,資源的消耗(CPU,、磁盤讀寫繁忙)、耗時也會越來越高,。 目前市面上的NoSQL基本上都支持分片存儲,,所以其天然分布式寫的能力從數(shù)據(jù)量上能得到非常的解決方案。而關(guān)系型數(shù)據(jù)庫,,查找算法與存儲結(jié)構(gòu)是可以優(yōu)化的空間比較少,,因此咱們一般思考出發(fā)點(diǎn)只有從如何減少數(shù)據(jù)量的這個角度進(jìn)行選擇優(yōu)化,因此本類型的優(yōu)化方案主要針對關(guān)系型數(shù)據(jù)庫進(jìn)行處理,。 數(shù)據(jù)歸檔注意點(diǎn):別一次性遷移數(shù)量過多,,建議低頻率多次限量遷移。像MySQL由于刪除數(shù)據(jù)后是不會釋放空間的,,可以執(zhí)行命令OPTIMIZE TABLE釋放存儲空間,,但是會鎖表,如果存儲空間還滿足,,可以不執(zhí)行,。 建議優(yōu)先考慮該方案,主要通過數(shù)據(jù)庫作業(yè)把非熱點(diǎn)數(shù)據(jù)遷移到歷史表,,如果需要查歷史數(shù)據(jù),,可新增業(yè)務(wù)入口路由到對應(yīng)的歷史表(庫),。 中間表(結(jié)果表)中間表(結(jié)果表)其實(shí)就是利用調(diào)度任務(wù)把復(fù)雜查詢的結(jié)果跑出來存儲到一張額外的物理表,因為這張物理表存放的是通過跑批匯總后的數(shù)據(jù),,因此可以理解成根據(jù)原有的業(yè)務(wù)進(jìn)行了高度的數(shù)據(jù)壓縮,。 以報表為例,如果一個月的源數(shù)據(jù)有數(shù)十萬,,我們通過調(diào)度任務(wù)以月的維度生成,,那么等于把原有的數(shù)據(jù)壓縮了幾十萬分之一;接下來的季報和年報可以根據(jù) 那么數(shù)據(jù)的壓縮比率是否越低越好?下面有一段口訣:
數(shù)據(jù)序列化存儲在數(shù)據(jù)庫以序列化存儲的方式,對于一些不需要結(jié)構(gòu)化存儲的業(yè)務(wù)來說是一種很好減少數(shù)據(jù)量的方式,,特別是對于一些 這種方案我認(rèn)為屬于一種臨時性的優(yōu)化方案,,無論是從序列化后丟失了部份字段的查詢能力,還是這方案的可優(yōu)化性都是有限的,。 分庫分表分庫分表作為數(shù)據(jù)庫優(yōu)化的一種非常經(jīng)典的優(yōu)化方案,,特別是在以前NoSQL還不是很成熟的年代,這個方案就如救命草一般的存在,。 如今也有不少同行也會選擇這種優(yōu)化方式,,但是從我角度來看,分庫分表是一種優(yōu)化成本很大的方案,。這里我有幾個建議:
拆分方式只要涉及到這個拆,那么無論是微服務(wù)也好,,分庫分表也好,,拆分的方式主要分兩種:垂直拆分、水平拆分 ,。 垂直拆分更多是從業(yè)務(wù)角度進(jìn)行拆分 ,,主要是為了降低業(yè)務(wù)耦合度;此外以SQL Server為例,,一頁是8KB存儲,,如果在一張表里字段越多,一行數(shù)據(jù)自然占的空間就越大,,那么一頁數(shù)據(jù)所存儲的行數(shù)就自然越少,,那么每次查詢所需要IO則越高因此性能自然也越慢;因此反之,,減少字段也能很好提高性能,。之前我聽說某些同行的表有80個字段,幾百萬的數(shù)據(jù)就開始慢了,。 水平拆分更多是從技術(shù)角度進(jìn)行拆分 ,,拆分后每張表的結(jié)構(gòu)是一模一樣的,簡而言之就是把原有一張表的數(shù)據(jù),,通過技術(shù)手段進(jìn)行分片到多張表存儲,,從根本上解決了數(shù)據(jù)量的問題。 路由方式進(jìn)行水平拆分后,,根據(jù)分區(qū)鍵(sharding key)原來應(yīng)該在同一張表的數(shù)據(jù)拆解寫到不同的物理表里,,那么查詢也得根據(jù)分區(qū)鍵進(jìn)行定位到對應(yīng)的物理表從而把數(shù)據(jù)給查詢出來。 路由方式一般有三種區(qū)間范圍,、Hash,、分片映射表,每種路由方式都有自己的優(yōu)點(diǎn)和缺點(diǎn),,可以根據(jù)對應(yīng)的業(yè)務(wù)場景進(jìn)行選擇,。 區(qū)間范圍根據(jù)某個元素的區(qū)間的進(jìn)行拆分,,以時間為例子,假如有個業(yè)務(wù)我們希望以月為單位拆分那么表就會拆分像 Hash也是一種常用的路由方式,根據(jù)Hash算法取模以數(shù)據(jù)量均勻分別存儲在物理表里,,缺點(diǎn)是對于帶分區(qū)鍵的查詢依賴特別強(qiáng),,如果不帶分區(qū)鍵就無法定位到具體的物理表導(dǎo)致相關(guān)所有表都查詢一次,而且在分庫的情況下對于Join,、聚合計算,、分頁等一些RDBMS的特性功能還無法使用。 一般分區(qū)鍵就一個,,假如有時候業(yè)務(wù)場景得用不是分區(qū)鍵的字段進(jìn)行查詢,,那么難道就必須得全部掃描一遍?其實(shí)可以使用分片映射表的方式,,簡單來說就是額外有一張表記錄額外字段與分區(qū)鍵的映射關(guān)系,。 舉個例子,有張訂單表,,原本是以UserID作為分區(qū)鍵拆分的,,現(xiàn)在希望用OrderID進(jìn)行查詢,那么得有額外得一張物理表記錄了OrderID與UserID的映射關(guān)系,。因此得先查詢一次映射表拿到分區(qū)鍵,,再根據(jù)分區(qū)鍵的值路由到對應(yīng)的物理表查詢出來。 可能有些朋友會問,,那這映射表是否多一個映射關(guān)系就多一張表,,還是多個映射關(guān)系在同一張表。我優(yōu)先建議單獨(dú)處理,,如果說映射表字段過多,,那跟不進(jìn)行水平拆分時的狀態(tài)其實(shí)就是一致的,這又跑回去的老問題,。 用空間換性能該類型的兩個方案都是用來應(yīng)對高負(fù)載的場景,,方案有以下兩種:分布式緩存、一主多從。 與其說這個方案叫用空間換性能,,我認(rèn)為用空間換資源更加貼切一些,。因此兩個方案的本質(zhì)主要通數(shù)據(jù)冗余、集群等方式分擔(dān)負(fù)載壓力,。 對于關(guān)系型數(shù)據(jù)庫而言,,因為他的ACID特性讓它天生不支持寫的分布式存儲,但是它依然天然的支持分布式讀,。 分布式緩存緩存層級可以分好幾種:客戶端緩存,、API服務(wù)本地緩存和分布式緩存 ,,咱們這次只聊分布式緩存。一般我們選擇分布式緩存系統(tǒng)都會優(yōu)先選擇NoSQL的鍵值型數(shù)據(jù)庫,,例如Memcached,、Redis,,如今Redis的數(shù)據(jù)結(jié)構(gòu)多樣性,高性能,,易擴(kuò)展性也逐漸占據(jù)了分布式緩存的主導(dǎo)地位,。 緩存策略也主要有很多種: 我相信大家對分布式緩存相對都比較熟悉了,,但是我在這里還是有幾個注意點(diǎn)希望提醒一下大家: 避免濫用緩存緩存應(yīng)該是按需使用,,從28法則來看,80%的性能問題由主要的20%的功能引起,。濫用緩存的后果會導(dǎo)致維護(hù)成本增大,,而且有一些數(shù)據(jù)一致性的問題也不好定位。 特別像一些動態(tài)條件的查詢或者分頁,,key的組裝是多樣化的,,量大又不好用keys指令去處理,當(dāng)然我們可以用額外的一個key把記錄數(shù)據(jù)的key以集合方式存儲,,刪除時候做兩次查詢,,先查Key的集合,然后再遍歷Key集合把對應(yīng)的內(nèi)容刪除,。這一頓操作下來無疑是非常廢功夫的,,誰弄誰知道。 避免緩存擊穿當(dāng)緩存沒有數(shù)據(jù),,就得跑去數(shù)據(jù)庫查詢出來,,這就是緩存穿透。假如某個時間臨界點(diǎn)數(shù)據(jù)是空的例如周排行榜,穿透過去的無論查找多少次數(shù)據(jù)庫仍然是空,,而且該查詢消耗CPU相對比較高,,并發(fā)一進(jìn)來因為缺少了緩存層的對高并發(fā)的應(yīng)對,這個時候就會因為并發(fā)導(dǎo)致數(shù)據(jù)庫資源消耗過高,,這就是緩存擊穿,。數(shù)據(jù)庫資源消耗過高就會導(dǎo)致其他查詢超時等問題。 該問題的解決方案也簡單,,對于查詢到數(shù)據(jù)庫的空結(jié)果也緩存起來,,但是給一個相對快過期的時間。有些同行可能又會問,,這樣不就會造成了數(shù)據(jù)不一致了么,?一般有數(shù)據(jù)同步的方案像分布式緩存、后續(xù)會說的一主多從,、CQRS,,只要存在數(shù)據(jù)同步這幾個字,那就意味著會存在數(shù)據(jù)一致性的問題,,因此如果使用上述方案,,對應(yīng)的業(yè)務(wù)場景應(yīng)允許容忍一定的數(shù)據(jù)不一致。 不是所有慢查詢都適用一般來說,,慢的查詢都意味著比較吃資源的(CPU,、磁盤I/O)。舉個例子,,假如某個查詢功能需要3秒時間,,串行查詢的時候并沒什么問題,我們繼續(xù)假設(shè)這功能每秒大概QPS為100,,那么在第一次查詢結(jié)果返回之前,,接下來的所有查詢都應(yīng)該穿透到數(shù)據(jù)庫,也就意味著這幾秒時間有300個請求到數(shù)據(jù)庫,,如果這個時候數(shù)據(jù)庫CPU達(dá)到了100%,,那么接下來的所有查詢都會超時,也就是無法有第一個查詢結(jié)果緩存起來,,從而還是形成了緩存擊穿,。 一主多從常用的分擔(dān)數(shù)據(jù)庫壓力還有一種常用做法,就是讀寫分離,、一主多從 ,。咱們都是知道關(guān)系型數(shù)據(jù)庫天生是不具備分布式分片存儲的,也就是不支持分布式寫,,但是它天然的支持分布式讀,。 一主多從是部署多臺從庫只讀實(shí)例,,通過冗余主庫的數(shù)據(jù)來分擔(dān)讀請求的壓力,路由算法可有代碼實(shí)現(xiàn)或者中間件解決,,具體可以根據(jù)團(tuán)隊的運(yùn)維能力與代碼組件支持視情況選擇,。 一主多從在還沒找到根治方案前是一個非常好的應(yīng)急解決方案,特別是在現(xiàn)在云服務(wù)的年代,,擴(kuò)展從庫是一件非常方便的事情,,而且一般情況只需要運(yùn)維或者DBA解決就行,無需開發(fā)人員接入,。 當(dāng)然這方案也有缺點(diǎn),,因為數(shù)據(jù)無法分片,所以主從的數(shù)據(jù)量完全冗余過去,,也會導(dǎo)致高的硬件成本,。從庫也有其上限,從庫過多了會主庫的多線程同步數(shù)據(jù)的壓力,。 選擇合適的存儲系統(tǒng)NoSQL主要以下五種類型:鍵值型,、文檔型、列型,、圖型、搜素引擎 ,,不同的存儲系統(tǒng)直接決定了查找算法,、存儲數(shù)據(jù)結(jié)構(gòu),也應(yīng)對了需要解決的不同的業(yè)務(wù)場景,。NoSQL的出現(xiàn)也解決了關(guān)系型數(shù)據(jù)庫之前面臨的難題(性能,、高并發(fā)、擴(kuò)展性等),。
因此本類型的方案主要有兩種:CQRS、替換(選擇)存儲 ,,這兩種方案的最終本質(zhì)基本是一樣的主要使用合適存儲來彌補(bǔ)關(guān)系型數(shù)據(jù)庫的缺點(diǎn),,只不過切換過渡的方式會有點(diǎn)不一樣。 CQRSCQS(命令查詢分離)指同一個對象中作為查詢或者命令的方法,,每個方法或者返回的狀態(tài),,要么改變狀態(tài),但不能兩者兼?zhèn)?/p> 講解CQRS前得了解CQS,有些小伙伴看了估計還沒不是很清晰,,我這里用通俗的話解釋:某個對象的數(shù)據(jù)訪問的方法里,,要么只是查詢,要么只是寫入(更新),。而CQRS(命令查詢職責(zé)分離)基于CQS的基礎(chǔ)上,,用物理數(shù)據(jù)庫來寫入(更新),而用另外的存儲系統(tǒng)來查詢數(shù)據(jù),。 因此我們在某些業(yè)務(wù)場景進(jìn)行存儲架構(gòu)設(shè)計時,,可以通過關(guān)系型數(shù)據(jù)庫的ACID特性進(jìn)行數(shù)據(jù)的更新與寫入,用NoSQL的高性能與擴(kuò)展性進(jìn)行數(shù)據(jù)的查詢處理,,這樣的好處就是關(guān)系型數(shù)據(jù)庫和NoSQL的優(yōu)點(diǎn)都可以兼得,,同時對于某些業(yè)務(wù)不適于一刀切的替換存儲的也可以有一個平滑的過渡。 從代碼實(shí)現(xiàn)角度來看,,不同的存儲系統(tǒng)只是調(diào)用對應(yīng)的接口API,,因此CQRS的難點(diǎn)主要在于如何進(jìn)行數(shù)據(jù)同步。 數(shù)據(jù)同步方式一般討論到數(shù)據(jù)同步的方式主要是分推和拉:
而推的方式又分兩種:CDC(變更數(shù)據(jù)捕獲)和領(lǐng)域事件,。對于一些舊的項目來說,,某些業(yè)務(wù)的數(shù)據(jù)入口非常多,無法完整清晰的梳理清楚,,這個時候CDC就是一種非常好的方式,,只要從最底層數(shù)據(jù)庫層面把變更記錄取到就可。 對于已經(jīng)服務(wù)化的項目來說領(lǐng)域事件是一種比較舒服的方式,,因為CDC是需要數(shù)據(jù)庫額外開啟功能或者部署額外的中間件,,而領(lǐng)域事件則不需要,從代碼可讀性來看會更高,,也比較開發(fā)人員的維護(hù)思維模式,。 替換(選擇)存儲系統(tǒng)因為從本質(zhì)來看該模式與CQRS的核心本質(zhì)是一樣的,主要是要對NoSQL的優(yōu)缺點(diǎn)有一個全面認(rèn)識,,這樣才能在對應(yīng)業(yè)務(wù)場景選擇與判斷出一個合適的存儲系統(tǒng),。這里我像大家介紹一本書馬丁.福勒《NoSQL精粹》,這本書我重復(fù)看了好幾遍,,也很好全面介紹各種NoSQL優(yōu)缺點(diǎn)和使用場景,。 當(dāng)然替換存儲的時候,,我這里也有個建議:加入一個中間版本,該版本做好數(shù)據(jù)同步與業(yè)務(wù)開關(guān),,數(shù)據(jù)同步要保證全量與增加的處理,,隨時可以重來,業(yè)務(wù)開關(guān)主要是為了后續(xù)版本的更新做的一個臨時型的功能,,主要避免后續(xù)版本更新不順利或者因為版本更新時導(dǎo)致的數(shù)據(jù)不一致的情況出現(xiàn),。在跑了一段時間后,驗證了兩個不同的存儲系統(tǒng)數(shù)據(jù)是一致的后,,接下來就可以把數(shù)據(jù)訪問層的底層調(diào)用替換了,。如此一來就可以平滑的更新切換。 結(jié)束本文到這里就把八大方案介紹完了,,在這里再次提醒一句,,每個方案都有屬于它的應(yīng)對場景,咱們只能根據(jù)業(yè)務(wù)場景選擇對應(yīng)的解決方案,,沒有通吃,,沒有銀彈。 這八個方案里,,大部分都存在數(shù)據(jù)同步的情況,,只要存在數(shù)據(jù)同步,無論是一主多從,、分布式緩存,、CQRS都好,都會有數(shù)據(jù)一致性的問題導(dǎo)致,,因此這些方案更多適合一些只讀的業(yè)務(wù)場景。當(dāng)然有些寫后既查的場景,,可以通過過渡頁或者廣告頁通過用戶點(diǎn)擊關(guān)閉切換頁面的方式來緩解數(shù)據(jù)不一致性的情況,。 |
|