1.1 數(shù)據(jù)庫性能瓶頸的出現(xiàn) 對于應(yīng)用來說,如果數(shù)據(jù)庫性能出現(xiàn)問題,要么是無法獲取連接,是因為在高并發(fā)的情況下連接數(shù)不夠了。要么是操作數(shù)據(jù)變慢,數(shù)據(jù)庫處理數(shù)據(jù)的效率除了問題。要么 是存儲出現(xiàn)問題,比如單機存儲的數(shù)據(jù)量太大了,存儲的問題也可能會導致性能的問題。歸根結(jié)底都是受到了硬件的限制,比如 CPU,內(nèi)存,磁盤,網(wǎng)絡(luò)等等。但是我們優(yōu) 化肯定不可能直接從擴展硬件入手,因為帶來的收益和成本投入比例太比。所以我們先來分析一下,當我們處理數(shù)據(jù)出現(xiàn)無法連接,或者變慢的問題的時候, 我們可以從哪些層面入手。 1.2 數(shù)據(jù)庫優(yōu)化方案對比 數(shù)據(jù)庫優(yōu)化有很多層面。 1.2.1 SQL 與索引 因為 SQL 語句是在我們的應(yīng)用端編寫的,所以第一步,我們可以在程序中對 SQL 語句進行優(yōu)化,最終的目標是用到索引。這個是容易的也是最常用的優(yōu)化手段。 1.2.2 表與存儲引擎 第二步,數(shù)據(jù)是存放在表里面的,表又是以不同的格式存放在存儲引擎中的,所以我們可以選用特定的存儲引擎,或者對表進行分區(qū),對表結(jié)構(gòu)進行拆分或者冗余處理, 或者對表結(jié)構(gòu)比如字段的定義進行優(yōu)化。 1.2.3 架構(gòu) 第三步,對于數(shù)據(jù)庫的服務(wù),我們可以對它的架構(gòu)進行優(yōu)化。如果只有一臺數(shù)據(jù)庫的服務(wù)器,我們可以運行多個實例,做集群的方案,做負載均衡?;蛘呋谥鲝膹?fù)制實現(xiàn)讀寫分離,讓寫的服務(wù)都訪問 master 服務(wù)器,讀的請求都訪 問從服務(wù)器,slave 服務(wù)器自動 master 主服務(wù)器同步數(shù)據(jù)?;蛘咴跀?shù)據(jù)庫前面加一層緩存,達到減少數(shù)據(jù)庫的壓力,提升訪問速度的目的。為了分散數(shù)據(jù)庫服務(wù)的存儲壓力和訪問壓力,我們也可以把不同的數(shù)據(jù)分布到不同的服務(wù)節(jié)點, 這個就是分庫分表(scale out)。注意主從(replicate)和分片(shard)的區(qū)別:主從通過數(shù)據(jù)冗余實現(xiàn)高可用,和實現(xiàn)讀寫分離。分片通過拆分數(shù)據(jù)分散存儲和訪問壓力。 1.2.4 配置 第四步,是數(shù)據(jù)庫配置的優(yōu)化,比如連接數(shù),緩沖區(qū)大小等等,優(yōu)化配置的目的都是為了更高效地利用硬件。 1.2.5 操作系統(tǒng)與硬件 最后一步操作系統(tǒng)和硬件的優(yōu)化。 從上往下,成本收益比慢慢地在增加。所以肯定不是查詢一慢就堆硬件,堆硬件叫做向上的擴展(scale up)。 什么時候才需要分庫分表呢?我們的評判標準是什么?如果是數(shù)據(jù)量的話,一張表存儲了多少數(shù)據(jù)的時候,才需要考慮分庫分表? 如果是數(shù)據(jù)增長速度的話,每天產(chǎn)生多少數(shù)據(jù),才需要考慮做分庫分表?如果是應(yīng)用的訪問情況的話,查詢超過了多少時間,有多少請求無法獲取連接,才需要分庫分表? 這是一個值得思考的問題。 1.3 架構(gòu)演進與分庫分表 1.3.1 單應(yīng)用單數(shù)據(jù)庫 2013 年的時候,我們公司采購了一個消費金融核心系統(tǒng),這個是一個典型的單體架構(gòu)的應(yīng)用。同學們應(yīng)該也很熟悉,單體架構(gòu)應(yīng)用的特點就是所有的代碼都在一個工程里面,打成 一個 war 包部署到 tomcat,最后運行在一個進程中。 這套消費金融的核心系統(tǒng),用的是 Oracle 的數(shù)據(jù)庫,初始化以后有幾百張表,比如客戶信息表、賬戶表、商戶表、產(chǎn)品表、放款表、還款表等等。 為了適應(yīng)業(yè)務(wù)的發(fā)展,我們這一套系統(tǒng)不停地在修改,代碼量越來越大,系統(tǒng)變得越來越臃腫。為了優(yōu)化系統(tǒng),我們搭集群,負載均衡,加緩存,優(yōu)化數(shù)據(jù)庫,優(yōu)化業(yè)務(wù)代碼系統(tǒng),但是都應(yīng)對不了系統(tǒng)的訪問壓力。 所以這個時候系統(tǒng)拆分就勢在必行了。我們把以前這一套采購的核心系統(tǒng)拆分出來很多的子系統(tǒng),比如提單系統(tǒng)、商戶管理系統(tǒng)、信審系統(tǒng)、合同系統(tǒng)、代扣系統(tǒng)、催收系統(tǒng),所有的系統(tǒng)都依舊共用一套 Oracle 數(shù)據(jù)庫。 1.3.2 多應(yīng)用單數(shù)據(jù)庫 對代碼進行了解耦,職責進行了拆分,生產(chǎn)環(huán)境出現(xiàn)問題的時候,可以快速地排查和解決 這種多個子系統(tǒng)共用一個 DB 的架構(gòu),會出現(xiàn)一些問題。第一個就是所有的業(yè)務(wù)系統(tǒng)都共用一個 DB,無論是從性能還是存儲的角度來說,都是滿足不了需求的。隨著我們的業(yè)務(wù)繼續(xù)膨脹,我們又會增加更多的系統(tǒng)來訪問核心數(shù) 據(jù)庫,但是一個物理數(shù)據(jù)庫能夠支撐的并發(fā)量是有限的,所有的業(yè)務(wù)系統(tǒng)之間還會產(chǎn)生競爭,最終會導致應(yīng)用的性能下降,甚至拖垮業(yè)務(wù)系統(tǒng)。 所以,分庫其實是我們在解決系統(tǒng)性能問題的過程中,對系統(tǒng)進行拆分的時候帶來的一個必然的結(jié)果?,F(xiàn)在的微服務(wù)架構(gòu)也是一 樣的,只拆應(yīng)用不拆分數(shù)據(jù)庫,不能解決根本的問題。 1.3.4 什么時候分表 當我們對原來一個數(shù)據(jù)庫的表做了分庫以后,其中一些表的數(shù)據(jù)還在以一個非???/em>這個時候查詢也已經(jīng)出現(xiàn)了非常明顯的效率下降, 所以,在分庫之后,還需要進一步進行分表。當然,我們最開始想到的可能是在一個數(shù)據(jù)庫里面拆分數(shù)據(jù),分區(qū)或者分表,到后面才是切分到多個數(shù)據(jù)庫中。 分表主要是為了減少單張表的大小,解決單表數(shù)據(jù)量帶來的性能問題 我們需要清楚的是,分庫分表會提升系統(tǒng)的復(fù)雜度,如果在近期或者未來一段時間內(nèi)必須要解決存儲和性能的問題,就不要去做超前設(shè)計和過度設(shè)計。就像我們搭建項目,從快速實現(xiàn)的角度來說,肯定是從單體項目起步的,在業(yè)務(wù)豐富完善之前,也用不到微服務(wù)架構(gòu)。如果我們創(chuàng)建的表結(jié)構(gòu)合理,字段不是太多,并且索引創(chuàng)建正確的情況下,單張表存儲幾千萬的數(shù)據(jù)是完全沒有問題的,這個還是以應(yīng)用的實際情況為準。當然我們也會對未來一段時間的業(yè)務(wù)發(fā)展做一個預(yù)判 2 分庫分表的類型和特點 從維度來說分成兩種,一種是垂直,一種是水平。 垂直切分:基于表或字段劃分,表結(jié)構(gòu)不同。我們有單庫的分表,也有多庫的分庫。 水平切分:基于數(shù)據(jù)劃分,表結(jié)構(gòu)相同,數(shù)據(jù)不同,也有同庫的水平切分和多庫的切分。 2.1 垂直切分 垂直分表有兩種,一種是單庫的,一種是多庫的。 2.1.1 單庫垂直分表 單庫分表,比如:商戶信息表,拆分成基本信息表,聯(lián)系方式表,結(jié)算信息表,附件表等等。 2.1.2 多庫垂直分表 多庫垂直分表就是把原來存儲在一個庫的不同的表,拆分到不同的數(shù)據(jù)庫。 比如:消費金融核心系統(tǒng)數(shù)據(jù)庫,有很多客戶相關(guān)的表,這些客戶相關(guān)的表,全部單獨存放到客戶的數(shù)據(jù)庫里面。合同,放款,風控相關(guān)的業(yè)務(wù)表也是一樣的。 當我們對原來的一張表做了分庫的處理,如果某些業(yè)務(wù)系統(tǒng)的數(shù)據(jù)還是有一個非常快的增長速度,比如說還款數(shù)據(jù)庫的還款歷史表,數(shù)據(jù)量達到了幾個億,這個時候硬件 限制導致的性能問題還是會出現(xiàn),所以從這個角度來說垂直切分并沒有從根本上解決單庫單表數(shù)據(jù)量過大的問題。在這個時候,我們還需要對我們的數(shù)據(jù)做一個水平的切分。 2.2 水平切分 當我們的客戶表數(shù)量已經(jīng)到達數(shù)千萬甚至上億的時候,單表的存儲容量和查詢效率都會出現(xiàn)問題,我們需要進一步對單張表的數(shù)據(jù)進行水平切分。水平切分的每個數(shù)據(jù)庫的表結(jié)構(gòu)都是一樣的,只是存儲的數(shù)據(jù)不一樣,比如每個庫存儲 1000 萬的數(shù)據(jù)。水平切分也可以分成兩種,一種是單庫的,一種是多庫的。 2.2.1 單庫水平分表 銀行的交易流水表,所有進出的交易都需要登記這張表,因為絕大部分時候客戶都是查詢當天的交易和一個月以內(nèi)的交易數(shù)據(jù),所以我們根據(jù)使用頻率把這張表拆分成三張表: 當天表:只存儲當天的數(shù)據(jù)。 當月表:在夜間運行一個定時任務(wù),前一天的數(shù)據(jù),全部遷移到當月表。用的是 insert into select,然后 delete。 歷史表:同樣是通過定時任務(wù),把登記時間超過 30 天的數(shù)據(jù),遷移到 history 歷史表(歷史表的數(shù)據(jù)非常大,我們按照月度,每個月建立分區(qū))。 費用表: 消費金融公司跟線下商戶合作,給客戶辦理了貸款以后,消費金融公司要給商戶返 費用,或者叫提成,每天都會產(chǎn)生很多的費用的數(shù)據(jù)。為了方便管理,我們每個月建立一張費用表,例如 fee_detail_201901……fee_detail_201912 但是注意,跟分區(qū)一樣,這種方式雖然可以一定程度解決單表查詢性能的問題,但是并不能解決單機存儲瓶頸的問題 2.2.2 多庫水平分表 另一種是多庫的水平分表。比如客戶表,我們拆分到多個庫存儲,表結(jié)構(gòu)是完全一樣的。 一般我們說的分庫分表都是跨庫的分表。既然分庫分表能夠幫助我們解決性能的問題,那我們是不是馬上動手去做,甚至在項目設(shè)計的時候就先給它分幾個庫呢?先冷靜 一下,我們來看一下分庫分表會帶來哪些問題,也就是我們前面說的分庫分表之后帶來的復(fù)雜性。 2.3 多案分庫分表帶來的問題 2.3.1 跨庫關(guān)聯(lián)查詢 比如查詢在合同信息的時候要關(guān)聯(lián)客戶數(shù)據(jù),由于是合同數(shù)據(jù)和客戶數(shù)據(jù)是在不同的數(shù)據(jù)庫,那么我們肯定不能直接使用 join 的這種方式去做關(guān)聯(lián)查詢。 我們有幾種主要的解決方案: 1、字段冗余 比如我們查詢合同庫的合同表的時候需要關(guān)聯(lián)客戶庫的客戶表,我們可以直接把一些經(jīng)常關(guān)聯(lián)查詢的客戶字段放到合同表,通過這種方式避免跨庫關(guān)聯(lián)查詢的問題。 2、數(shù)據(jù)同步:比如商戶系統(tǒng)要查詢產(chǎn)品系統(tǒng)的產(chǎn)品表,我們干脆在商戶系統(tǒng)創(chuàng)建一張產(chǎn)品表,通過 ETL 或者其他方式定時同步產(chǎn)品數(shù)據(jù)。 3、全局表(廣播表) 比如行名行號信息被很多業(yè)務(wù)系統(tǒng)用到,如果我們放在核心系統(tǒng),每個系統(tǒng)都要去關(guān)聯(lián)查詢,這個時候我們可以在所有的數(shù)據(jù)庫都存儲相同的基礎(chǔ)數(shù)據(jù)。 4、ER 表(綁定表) 我們有些表的數(shù)據(jù)是存在邏輯的主外鍵關(guān)系的,比如訂單表 order_info,存的是匯總的商品數(shù),商品金額;訂單明細表 order_detail,是每個商品的價格,個數(shù)等等?;蛘?/p> 叫做從屬關(guān)系,父表和子表的關(guān)系。他們之間會經(jīng)常有關(guān)聯(lián)查詢的操作,如果父表的數(shù)據(jù)和子表的數(shù)據(jù)分別存儲在不同的數(shù)據(jù)庫,跨庫關(guān)聯(lián)查詢也比較麻煩。所以我們能不能 把父表和數(shù)據(jù)和從屬于父表的數(shù)據(jù)落到一個節(jié)點上呢? 比如 order_id=1001 的數(shù)據(jù)在 node1,它所有的明細數(shù)據(jù)也放到 node1;order_id=1002 的數(shù)據(jù)在 node2,它所有的明細數(shù)據(jù)都放到 node2,這樣在關(guān)聯(lián)查詢的時候依然是在 一個數(shù)據(jù)庫。 上面的思路都是通過合理的數(shù)據(jù)分布避免跨庫關(guān)聯(lián)查詢,實際上在我們的業(yè)務(wù)中,也是盡量不要用跨庫關(guān)聯(lián)查詢,如果出現(xiàn)了這種情況,就要分析一 下業(yè)務(wù)或者數(shù)據(jù)拆分是不是合理。如果還是出現(xiàn)了需要跨庫關(guān)聯(lián)的情況,那我們就只能用最后一種辦法 5、系統(tǒng)層組裝 在不同的數(shù)據(jù)庫節(jié)點把符合條件數(shù)據(jù)的數(shù)據(jù)查詢出來,然后重新組裝,返回給客戶端 2.3.2 分布式事務(wù) 比如在一個貸款的流程里面,合同系統(tǒng)登記了數(shù)據(jù),放款系統(tǒng)也必須生成放款記錄,如果兩個動作不是同時成功或者同時失敗,就會出現(xiàn)數(shù)據(jù)一致性的問題。如果在 一個數(shù)據(jù)庫里面,我們可以用本地事務(wù)來控制,但是在不同的數(shù)據(jù)庫里面就不行了。所以分布式環(huán)境里面的事務(wù),我們也需要通過一些方案來解決。復(fù)習一下。分布式系統(tǒng)的基礎(chǔ)是 CAP 理論。 1.C (一致性) Consistency:對某個指定的客戶端來說,讀操作能返回最新的寫操作。對于數(shù)據(jù)分布在不同節(jié)點上的數(shù)據(jù)來說,如果在某個節(jié)點更新了數(shù)據(jù),那么在其他節(jié)點 如果都能讀取到這個最新的數(shù)據(jù),那么就稱為強一致,如果有某個節(jié)點沒有讀取到,那就是分布式不一致。 2.A (可用性) Availability:非故障的節(jié)點在合理的時間內(nèi)返回合理的響應(yīng)(不是錯誤和超時的響應(yīng))??捎眯缘膬蓚€關(guān)鍵一 個是合理的時間,一個是合理的響應(yīng)。合理的時間指的是請求不能無限被阻塞,應(yīng)該在合理的時間給出返回。合理的響應(yīng)指的是系統(tǒng)應(yīng)該明確返回結(jié)果并且結(jié)果是正確的 3.P (分區(qū)容錯性) Partition tolerance:當出現(xiàn)網(wǎng)絡(luò)分區(qū)后,系統(tǒng)能夠繼續(xù)工作。打個比方,這里集群有多臺機器,有臺機器網(wǎng)絡(luò)出現(xiàn)了問題,但是這個集群仍然可以正工作。 CAP 三者是不能共有的,只能同時滿足其中兩點。基于 AP,我們又有了 BASE 理論?;究捎?Basically Available):分布式系統(tǒng)在出現(xiàn)故障時,允許損失部分可用功能,保證核心功能可用。 軟狀態(tài)(Soft state):允許系統(tǒng)中存在中間狀態(tài),這個狀態(tài)不影響系統(tǒng)可用性,這里指的是 CAP 中的不一致。 最終一致(Eventually consistent):最終一致是指經(jīng)過一段時間后,所有節(jié)點數(shù)據(jù)都將會達到一致 分布式事務(wù)有幾種常見的解決方案: 1、全局事務(wù)(比如 XA 兩階段提交;應(yīng)用、事務(wù)管理器(TM)、資源管理器(DB)), 例如 Atomikos 兩階段提交 2、基于可靠消息服務(wù)的分布式事務(wù) 3、柔性事務(wù) TCC(Try-Confirm-Cancel)tcc-transaction 4、最大努力通知,通過消息中間件向其他系統(tǒng)發(fā)送消息(重復(fù)投遞 定期校對) 2.3.3 排序、翻頁、函數(shù)計算問題 跨節(jié)點多庫進行查詢時,會出現(xiàn) limit 分頁,order by 排序的問題。比如有兩個節(jié)點,節(jié)點 1 存的是奇數(shù) id=1,3,5,7,9……;節(jié)點 2 存的是偶數(shù) id=2,4,6,8,10…… 執(zhí)行 select*from user_info order by id limit 0,10需要在兩個節(jié)點上各取出 10 條,然后合并數(shù)據(jù),重新排序。max、min、sum、count 之類的函數(shù)在進行計算的時候, 也需要先在每個分片上執(zhí)行相應(yīng)的函數(shù),然后將各個分片的結(jié)果集進行匯總和再次計算,最終將結(jié)果返回 2.3.4 全局主鍵避重問題 MySQL 的數(shù)據(jù)庫里面字段有一個自增的屬性,Oracle 也有 Sequence 序列。如果是一個數(shù)據(jù)庫,那么可以保證 ID 是不重復(fù)的,但是水平分表以后,每個表都按照自己的規(guī)律自增, 肯定會出現(xiàn) ID 重復(fù)的問題,這個時候我們就不能用本地自增的方式了。 我們有幾種常見的解決方案: 1)UUID(Universally Unique Identifier 通用唯一識別碼) UUID 標準形式包含 32 個 16 進制數(shù)字,分為 5 段,形式為 8-4-4-4-12 的 36 個字符,例如:c4e7956c-03e7-472c-8909-d733803e79a9 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx M 表示 UUID 版本,目前只有五個版本,即只會出現(xiàn) 1,2,3,4,5,數(shù)字 N 的一至三個最高有效位表示 UUID 變體,目前只會出現(xiàn) 8,9,a,b 四種情況。 1、基于時間和 MAC 地址的 UUID 2、基于第一版卻更安全的 DCE UUID 3、基于 MD5 散列算法的 UUID 4、基于隨機數(shù)的 UUID——用的最多,JDK 里面是 4 5、基于 SHA1 散列算法的 UUID UUID 是主鍵是最簡單的方案,本地生成,性能高,沒有網(wǎng)絡(luò)耗時。但缺點也很明顯,由于 UUID 非常長,會占用大量的存儲空間;另外,作為主鍵建立索引和基于索引進行 查詢時都會存在性能問題,在 InnoDB 中,UUID 的無序性會引起數(shù)據(jù)位置頻繁變動,導致分頁。 2) 數(shù)據(jù)庫 把序號維護在數(shù)據(jù)庫的一張表中。這張表記錄了全局主鍵的類型、位數(shù)、起始值,當前值。當其他應(yīng)用需要獲得全局 ID 時,先 for update 鎖行,取到值 1 后并且更新后 返回。并發(fā)性比較差。 3)Redis 基于 Redis 的 INT 自增的特性,使用批量的方式降低數(shù)據(jù)庫的寫壓力,每次獲取一 段區(qū)間的 ID 號段,用完之后再去數(shù)據(jù)庫獲取,可以大大減輕數(shù)據(jù)庫的壓力。 4)雪花算法 Snowflake(64bit)4)雪花算法 Snowflake(64bit) 核心思想: a)使用 41bit 作為毫秒數(shù),可以使用 69 年 b)10bit 作為機器的 ID(5bit 是數(shù)據(jù)中心,5bit 的機器 ID),支持 1024 個 節(jié)點 c)12bit 作為毫秒內(nèi)的流水號(每個節(jié)點在每毫秒可以產(chǎn)生 4096 個 ID) d)最后還有一個符號位,永遠是 0。 代碼:snowflake.SnowFlakeTest 優(yōu)點:毫秒數(shù)在高位,生成的 ID 整體上按時間趨勢遞增;不依賴第三方系統(tǒng),穩(wěn)定 性和效率較高,理論上 QPS 約為 409.6w/s(1000*2^12),并且整個分布式系統(tǒng)內(nèi)不會 產(chǎn)生 ID 碰撞;可根據(jù)自身業(yè)務(wù)靈活分配 bit 位。 不足就在于:強依賴機器時鐘,如果時鐘回撥,則可能導致生成 ID 重復(fù)。 當我們對數(shù)據(jù)做了切分,分布在不同的節(jié)點上存儲的時候,是不是意味著會產(chǎn)生多 個數(shù)據(jù)源?既然有了多個數(shù)據(jù)源,那么在我們的項目里面就要配置多個數(shù)據(jù)源。 2.4 多數(shù)據(jù)源/讀寫數(shù)據(jù)源的解決方案 我們先要分析一下 SQL 執(zhí)行經(jīng)過的流程。 DAO——Mapper(ORM)——JDBC——代理——數(shù)據(jù)庫服務(wù) 2.4.1 客戶端 DAO 層 2.4.4 代理層 前面三種都是在客戶端實現(xiàn)的,也就是說不同的項目都要做同樣的改動,不同的編 程語言也有不同的實現(xiàn), |
|