起因:公司項目的數據量過大,已經超過20T,單張表數據+索引近5T,單表及單庫性能都面臨巨大的挑戰。為了保證用戶體驗,提升效率,數據庫方面需要優化。
項目:分布式項目,單系統已做集群,日均查詢量2000W左右,交易量800W左右
特點:數據量大,並發量大
***(由於本身所在的項目屬於核心系統部分與數據庫交互,其他系統調用核心系統接口,所以不做闡述,僅記錄本系統做法)
數據庫:Oracle+Mysql
語言:Java
技術:zookeeper+dubbo+spring+mybatis+內部框架
分庫分表:mycat+內部中間件
===========================================分割線================================================
在闡述之前,要先說明一下:
1,項目在線上穩定運行,要保證用戶的服務,不能停止項目的運行
2,版本的切換要迅速,以最少的時間完成
3,代碼的改造要兼容舊系統以及其他系統
4,不能影響業務
5,平滑過渡
等等......
===========================================分割線================================================
數據庫部分
舊版本:原有的系統是采用單個oracle數據庫,所有的業務都指向同一個數據庫
新版本:將單個數據庫,依據業務的組成,拆分成為幾部分,如下圖
比如原先的一個系統,所有的業務都指向同一個數據庫
如
舊系統業務=(訂單+產品+用戶+....)DB (一個DB)
新系統業務=訂單DB+產品DB+用戶DB+.... (多個DB)
實際做法:我們的實際做法是依據業務,將所有的表划分到不同的歸屬域中,並不是一個業務一個庫,另外增加分庫分表的路由表,分區屬性,分庫屬性。
原因:根據業務,將不同的表划分到不同的歸屬域,要考慮到原系統的實際情況和新方案之間的一些過渡,兼容,可行性問題。
比如原代碼的改造,重構的工作量的大小;是否會影響到上游系統;舊表和新表之間的數據問題等等。
另外不同域代表的不僅是業務歸屬,還代表數據庫歸屬和表歸屬,為什么這么說呢?
因為原先一個庫根據業務的歸屬域,拆分成多個庫,用分區鍵路由,看你去哪一個歸屬域;在一個歸屬域中,一個庫又拆成多個庫,存儲不同數據,用分庫鍵路由,看你去哪一個數據庫,其實庫中的表結構都一樣,只是存放的數據不同。
以上是數據庫的拆分思路,要弄清楚一件事情,就是原本是所有的表都在一個庫里面,現在是把不同的表依據歸屬域,放到不同的庫里面,同一個歸屬域的數據庫,比如說有100個庫,那么100個庫里的表是一樣的,不同的庫的表雖然相同,但是數據是不同的。
===========================================分割線================================================
相關問題部分:
有人會說,原先的代碼,SQL指向的是同一個數據源,那么做了拆分之后,指向多個不同的數據源,那么會出問題
比如:
1,已經穩定運行的龐大系統,老代碼,新代碼,怎么知道哪些sql會在分庫分表的時候出現問題?
2,多表查詢的時候,不是同一個數據源怎么多表查詢?
3,多數據源之間怎么切換,保持事務?
4,原有業務代碼邏輯怎么改?
5,並發和防重怎么解決?
6,分庫分表的實施,肯定會改變原表的結構導致數據問題,怎么辦?
首先,要做前期的准備工作
===========================================分割線================================================
前期准備工作
1,已經穩定運行的龐大系統,老代碼,新代碼,怎么知道哪些sql會在分庫分表的時候出現問題,怎么知道需要修改哪些SQL?
方案:監控系統
思路:簡單的監控系統是由過濾器或者攔截器構成,而且本身mybatis可以配置打印sql
實際做法:其實我們的做法是新增監控系統(當然會復雜一些),采取為期一段時間(需要評估)的監控,每天搜集日志,整理,歸納,可以用python腳本。
把項目當中使用的sql整理出來,並且同時項目依據業務邏輯拆分成幾大塊,分配給不同的開發人員整理,歸納,與監控系統的結果做對比,把相關SQL整理出來。
2,多表查詢的時候,不是同一個數據源怎么多表查詢?
方案:SQL拆分+接口改造
思路:跨數據源的SQL拆分成多個SQL,業務代碼重新整理
實際做法:
a,根據業務的拆分, 分配給不同的開發人員整理,歸納業務接口
b,依據不同的接口,根據接口中的業務和SQL,對比整理出的正在使用的SQL
c,依據分庫邏輯,把跨數據源的多表關聯的SQL拆分成單個SQL,如果不跨數據源,依舊可以多表查詢
這里其實還涉及到接口的改造,因為在拆分SQL的時候,接口代碼也會有相應的改動,這就要分配任務了......總之很難受
3,多數據源之間怎么切換,保持事務?
方案:單數據源單事務
思路:跨數據源的SQL已經拆分為前提,多個單數據源的SQL分別為各自的事務
實際做法:由於有內部框架,我們自己封裝了jar包,手動開啟,提交,回滾事務
舉個例子:
舊系統:不管多少業務,不管什么業務,都是1個庫,庫里有A,B,C,D,E五張表,那么你查詢一下就直接關聯5張表就好了,需要update就直接update就好了,隨便怎么搞
新系統:分別是5個庫,5個業務,公共區域(品牌)+訂單+用戶+交易+積分一共5個業務,不同庫里的表不同(理一下之間的關系)
a,公共區域庫:品牌表A
b,訂單庫:訂單表B
c,用戶庫:用戶表C
d,交易庫:交易表D
e,積分庫:積分表E
比如一個業務:需要查詢某個品牌的某個時間段的訂單記錄和這些訂單的用戶信息
<1>,先把需要查詢的品牌先從表A中查詢出來
<2>,依據<1>中的結果,當做參數傳遞到訂單表中去查詢,並且帶上時間參數,獲取訂單信息
<3>,把<2>中的結果當做參數傳遞到用戶表中去查詢,獲取用戶信息
<4>,組裝需要的結果
再比如:用戶下單購買多件商品,根據用戶訂單的金額給用戶加積分
<1>,用戶下單,訂單表增加數據
<2>,交易表記錄流水
<3>,查詢這個用戶這比訂單總金額
<4>,把<3>的結果當做參數傳遞到積分表算積分
不同的庫,都分別開啟和提交各自的事務,通過程序來控制各個小事務
關於產生臟數據的問題如下:
比如有A,B,C,D4個庫,A中有1表,B中有2表,C中有3表,D中有4表,有一個業務需要做如下操作:
1,在A的1表中做select
2,把1的結果當做參數給B中2表,做insert
3,把1的結果當做參數給C中3表,做update
4,最后再把3的返回值,當做條件和參數,去D中4表做update
那么當3步驟出錯的話,我們肯定是會catch的,但是2步驟的事務是提交了的,那么怎么辦?
答案是:
1,3步驟出現錯誤,3步驟是可以回滾的,沒有問題
2,2步驟的話,數據需要刪除
那么又會問如果又出錯了怎么辦?
當然也確實有這個問題,我們的做法是有一個監控系統和補償機制。
比如制定規則之后,每隔半個小時去掃描一些表,看哪些數據是有問題的,依據補償的機制去補償。
因為其實在實際操作過程中會增加很多表來完成這些操作。
比如最常見的補償就是沖正了。
舉個例子(自行百度)
即一筆交易在終端已經置為成功標志,但是發送到主機的帳務交易包沒有得到響應,即終端交易超時,所以不確定該筆交易是否在主機端也成功完成,為了確保用戶的利益,終端重新向主機發送請求,請求取消該筆交易的流水,如果主機端已經交易成功,則回滾交易,否則不處理,然后將處理結果返回給終端。
4,分庫分表的實施,肯定會改變原表的結構導致數據問題,怎么辦?
方案:數據清洗。
思路:依據分庫分表的方案,確定新庫的表結構,確定增加或者刪除哪些字段,重新建立新表。在原表的基礎上,新增字段,去除臟數據。
實際作法:
1,會先清除一批臟數據,這些臟數據是由於業務原因所導致的一些歷史遺留問題,不會影響到現有的使用,比如說臨時賬戶之類,臨時數據之類。
2,保留可用數據,在現有表的基礎上增加分庫分表必要的字段
3,拆分原有表,比如說字段太多,可拆(看情況)
4,新增分庫分表必須表,比如說路由表
5,去除冗余,無效字段
===========================================分割線================================================
4,原有業務代碼邏輯怎么改
一般分庫分表,如果會拆表或者廢除一些表,那么就需要看代碼,把廢除的表的代碼和拆表的代碼重構,
原則上是需要多人協作仔細看代碼來做這件事情的,分工合作
5,關於並發和防重問題
其實網上有很多解決方法
我們采取的做法是
1,樂觀鎖
2,悲觀鎖
3,冪等性校驗
4,少量java代碼的鎖比如synchronized和lock鎖(一般采用前三個,這個耗性能)
===========================================分割線================================================
在完成切換的過程之前,其實是有一個過渡期的,什么叫過渡期?
1,代碼是一點一點寫的,不是一下能完成的
2,多個團隊協作,不是你的版本上線了,人家就要上線的
3,不同的團隊負責的東西不同
現實:
1,在數據庫的實際分庫分表部分是由DBA負責的
2,在和數據庫交互的過程中,是由中間件團隊負責的
3,核心系統部分其實只是修改自己的業務代碼和SQL語句來配合
舉個例子比如說,我們要把一個庫拆分成5個區域,每個區域都有10個庫,那么一共50個庫,怎么過度呢?
<1>,首先核心系統需要切換配置,首先要先配置好50個庫的文件,但是路徑指向的是同一個庫,這是在過渡
<2>,到了和中間件團隊約定的時間,由中間件團隊的中間件去路由各個庫
<3>,實際的切換不是同時切換到50個庫,而是一個一個的切,每個大約在5分鍾左右,在切庫的過程中,只允許查詢功能出現,停止一切寫入庫的操作
===========================================分割線================================================
關於路由問題:
雖然和數據庫的交互是由中間件的團隊來負責,但是我們也需要告訴中間件,到底哪一個連接是連哪一個數據庫的,不然中間件也不能識別到底哪一個請求是連A庫,哪一個請求去連B庫。
方案:增加分區鍵,分庫鍵
思路:請求參數中新增分區屬性,分庫屬性參數,通過此參數告訴中間件
實際做法:我們的實際做法是依據業務,將所有的表划分到不同的歸屬域中,並不是一個業務一個庫,另外增加分庫分表的路由表,分區屬性,分庫屬性。
原因:根據業務,將不同的表划分到不同的歸屬域,要考慮到原系統的實際情況和新方案之間的一些過渡,兼容,可行性問題。
比如原代碼的改造,重構的工作量的大小;是否會影響到上游系統;舊表和新表之間的數據問題等等。
另外不同域代表的不僅是業務歸屬,還代表數據庫歸屬和表歸屬,為什么這么說呢?
因為原先一個庫根據業務的歸屬域,拆分成多個庫,用分區鍵路由,看你去哪一個歸屬域;在一個歸屬域中,一個庫又拆成多個庫,存儲不同數據,用分庫鍵路由,看你去哪一個數據庫,其實庫中的表結構都一樣,只是存放的數據不同。
===========================================分割線================================================
關於SQL拆分問題
1,在大部分的時候,查詢的需求會多一些,所以需要查詢的速度很快,那么在優化的時候肯定有加索引這一條,但是我們在使用mybatis的時候經常會寫一些判斷語句,如下:
<if test="fwbdh != null and fwbdh == 'BAK'">
fwbdh=#{fwbdh}
<if>
這種語句呢其實會很尷尬,為什么呢?因為DBA在加索引的時候,他不知道你的SQL中的where條件里到底這個字段要不要加索引,因為這個字段可能查詢的時候不會用到。
所以,優化中的一條就是盡量不要寫這種SQL,能判斷的條件就在java代碼里判斷掉,或者把復雜SQL拆成簡單SQL,用很多簡單SQL來代替這些復雜SQL,說白了就是JAVA代碼里多寫if判斷。但是呢,if判斷多了也很耗性能,這也是很尷尬的,所以就權衡利弊了,不過一般來說,數據量超級大的時候,數據庫查詢的速度會比JAVA代碼的運行速度慢很多。
2,一條復雜SQL和多條簡單SQL執行一個業務
其實這個要分場景的,需要評估
3,千萬不要寫子查詢,尤其是嵌套子查詢
4,千萬不要寫select * from
5,常見優化方案百度
===========================================分割線================================================
關於中間件
我們分庫分表采用的是mycat,數據庫是mysql+oracle
其實這里采用的mycat已經不是開源的mycat了,中間件團隊已經修改了mycat的源碼
關於連接數據庫的問題
Mysql:大家都知道mysql是開源的,所以,中間件團隊選擇以協議的方式來連接mysql
Oracle:因為oracle不是開源的,所以中間件團隊選擇以jdbc的方式連接oracle
另外連接數據庫的連接池選用的是duird,因為duird連接池是開源的,所以中間件團隊也是修改了durid的源碼
在路由數據庫之前,我們已經和中間件團隊商討過路由策略,決定以業務表中的某個字段的數據作為標志,另外會把這個數據塞到線程中去,最后由中間件團隊來識別並處理。
===========================================分割線================================================
以上的方案實際上還在優化的過程中,但是已經是按照這個方向在走了,這個工程浩大,是以年來計算的,並不是一兩個月能完成的,走到某一步,下一步可能都需要優化,改進。