《筆者帶你剖析輕量級Sharding中間件——Kratos1.x》
之所以編寫Kratos其實(shí)存在一個小插曲,當(dāng)筆者滿山遍野尋找成熟、穩(wěn)定、高性能的Sharding中間件時,確實(shí)是翻山越嶺,只不過始終沒有找到一款合適筆者項目場景的中間件產(chǎn)品。依稀記得當(dāng)年第一款使用的Sharding中間件就是淘寶的TDDL3.0,只可惜現(xiàn)在拿不到源碼。而其它的中間件,大多都是基于Proxy的,相信做過分布式系統(tǒng)的人都知道,拋開網(wǎng)絡(luò)消耗所帶來的性能問題不談,多一個外圍系統(tǒng)依賴就意味著需要多增加和承擔(dān)一份風(fēng)險,因此與應(yīng)用集成的方式,則成為筆者選擇的首要條件。當(dāng)然不是說基于Proxy方式的Sharding中間件不好,只能說我個人并不看重通用型需求,僅此而已。其實(shí)目前社區(qū)非?;钴S的MyCat,筆者在這里要批評一下,既然開源,那么就請在GitHub上貼上使用手冊,而不是配置手冊,因?yàn)閷τ谝粋€任何一個DVP而言,他要的是迅速上手的幫助文檔,而不是要使用你們的開源產(chǎn)品還需要在淘寶上購買幾十塊一本的網(wǎng)絡(luò)書,對于這一點(diǎn),我非常鄙視和厭惡。
許多開發(fā)人員動不動就大談分庫分表來顯示自己的成就感,那么筆者需要做的事情就是將其拉下神壇,讓大家切切實(shí)實(shí)感受到一款平民化技術(shù)所帶來的親民性。作為一款數(shù)據(jù)路由工具,Kratos采用與應(yīng)用集成的方式為開發(fā)人員帶來超強(qiáng)的易用性。就目前而言,筆者測試環(huán)境上,物理環(huán)境選用為32庫和1024表的庫內(nèi)分片模式,因此筆者就先不必王婆賣瓜,自賣自夸其所謂的高性能和高穩(wěn)定。至于你是否能夠從Kratos中獲益,取決于你是否想用最簡單,最易上手的方式來解決分庫分表場景下的數(shù)據(jù)路由工作。
筆者此篇博文,并不是宣導(dǎo)和普及分庫分表理論,而是作為Kratos貨真價實(shí)的Sharding權(quán)威指南手冊呈現(xiàn)給大家。不過實(shí)在抱歉,目前Kratos暫無開源打算,只提供免費(fèi)試用,如果后期有開源計劃,會在第一時間發(fā)布在GitHub上。
目錄
一、Kratos簡介;
二、互聯(lián)網(wǎng)當(dāng)下的數(shù)據(jù)拆分過程;
三、目前市面上常見的一些Sharding中間件產(chǎn)品對比;
四、Kratos的架構(gòu)原型;
五、動態(tài)數(shù)據(jù)源層的Master/Slave讀寫分離;
六、Kratos的Sharding模型;
七、Sharding之庫內(nèi)分片;
八、Sharding之一庫一片;
九、自動生成全局唯一的sequenceId;
十、自動生成kratos分庫分表配置文件;
十一、注意事項;
一、Kratos簡介
因?yàn)檎也坏胶线m的Shading中間件,其次一些開源的Shading中間件動不動就幾萬行的代碼真有必要嗎?因此誕生了編寫自己中間件的想發(fā)。Kratos這個名字來源于筆者之前在PS3平臺玩的一款A(yù)CT游戲《戰(zhàn)神3》中嗜血?dú)⑸竦闹鹘强鼱擪ratos,盡管筆者的Kratos并沒有展現(xiàn)出秒殺其他Sharding中間件的霸氣,但Kratos要做的事情很純粹,僅僅就只是處理分庫分表場景下的數(shù)據(jù)路由工作,它處于數(shù)據(jù)路由層,介于持久層與JDBC之間,因此沒有其它看似有用實(shí)則花哨的雞肋功能,并且與應(yīng)用層集成的方式注定了Kratos必然擁有更好的易用性。
對于開發(fā)人員而言,在使用Kratos的時候,就好像是在操作同一個數(shù)據(jù)源一樣,也就是說,之前你的sql怎么寫,換上Kratos之后,業(yè)務(wù)邏輯無需做出變動,你只需要關(guān)心你的邏輯即可,數(shù)據(jù)路由工作,你完全可以放心的交由Kratos去處理。Kratos站在巨人的肩膀上,必然擁有更大的施展空間。首先Kratos依賴于Spring JDBC,那么如果你習(xí)慣使用JdbcTemplate,那就都不是問題,反之你可以先看一下筆者的博文《筆者帶你剖析Spring3.x JDBC》。其次Kratos目前僅僅只支持Mysql數(shù)據(jù)庫,對于其他RDBMS類型的數(shù)據(jù)庫,基本上以后也不會支持,簡單來說,通用型需求并不是Kratos的目標(biāo),做好一件事才是真正的實(shí)惠。
kratos的優(yōu)點(diǎn):
1、動態(tài)數(shù)據(jù)源的無縫切換;
2、master/slave一主一從讀寫分離;
3、單線程讀重試(取決于的數(shù)據(jù)庫連接池是否支持);
4、單獨(dú)支持Mysql數(shù)據(jù)庫;
5、非Proxy架構(gòu),與應(yīng)用集成,應(yīng)用直連數(shù)據(jù)庫,降低外圍系統(tǒng)依賴帶來的down機(jī)風(fēng)險;
6、使用簡單,侵入型低,站在巨人的肩膀上,依賴于Spring JDBC;
7、分庫分表路由算法支持2種分片模式,庫內(nèi)分片/一庫一片;
8、提供自動生成sequenceId的API支持;
9、提供自動生成配置文件的支持,降低配置出錯率;
二、互聯(lián)網(wǎng)當(dāng)下的數(shù)據(jù)拆分過程
對于一個剛上線的互聯(lián)網(wǎng)項目來說,由于前期活躍度并不大,并發(fā)量相對較小,因此企業(yè)一般都會選擇將所有數(shù)據(jù)存放在一個物理DB中進(jìn)行讀寫操作。但隨著后續(xù)的市場推廣力度不斷加強(qiáng),活躍度不斷提升,這時如果僅靠一個DB來支撐所有讀寫壓力,就會顯得力不從心。所以一般到了這個階段,大部分Mysql DBA就會將數(shù)據(jù)庫設(shè)置為讀寫分離狀態(tài)(一主一從/一主多從),Master負(fù)責(zé)寫操作,而Slave負(fù)責(zé)讀操作。按照二八定律,80%的操作更多是讀操作,那么剩下的20%則為寫操作,經(jīng)過讀寫分離后,大大提升了單庫無法支撐的負(fù)載壓力。不過光靠讀寫分離并不會一勞永逸,如果活躍度再次提升,相信又會再次遇見數(shù)據(jù)庫的讀寫瓶頸。因此到了這個階段,就需要實(shí)現(xiàn)垂直分庫。
所謂垂直分庫,簡單來說就是根據(jù)業(yè)務(wù)的不同將原本冗余在單庫中的業(yè)務(wù)表拆散,分布到不同的業(yè)務(wù)庫中,實(shí)現(xiàn)分而治之的讀寫訪問操作。當(dāng)然我們都知道,傳統(tǒng)RDBMS數(shù)據(jù)庫中的數(shù)據(jù)表隨著數(shù)據(jù)量的暴增,從維護(hù)性和高響應(yīng)的角度去看,無論任何CRUD操作,對于數(shù)據(jù)庫而言,都是一件極其傷腦筋的事情。即便設(shè)置了索引,檢索效率依然低下,因?yàn)殡S著數(shù)據(jù)量的暴增,RDBMS數(shù)據(jù)庫的性能瓶頸就會逐漸顯露出來。這一點(diǎn),Nosql數(shù)據(jù)庫倒是做得很好,當(dāng)然架構(gòu)不同,所以不予比較。那么既然已經(jīng)到了這個節(jié)骨眼上了,唯一的殺手锏就是在垂直分庫的基礎(chǔ)之上進(jìn)行水平分區(qū),也就是我們常說的Sharding。
簡單來說,水平分區(qū)要做的事情,就是將原本冗余在單庫中的業(yè)務(wù)表分散為N個子表(比如tab_0001、tab_0002、tab_0003、tab_0004...)分別存放在不同的子庫中。理論上來講,子表之間通過某種契約關(guān)聯(lián)在一起,每一張子表均按段位進(jìn)行數(shù)據(jù)存儲,比如tab_0000存儲1-10000的數(shù)據(jù),而tab_0001存儲10001-20000的數(shù)據(jù)。經(jīng)過水平分區(qū)后,必然能夠?qū)⒃居梢粡垬I(yè)務(wù)表維護(hù)的海量數(shù)據(jù)分配給N個子表進(jìn)行讀寫操作和維護(hù),大大提升了數(shù)據(jù)庫的讀寫性能,降低了性能瓶頸?;诜謳旆直淼脑O(shè)計,目前在國內(nèi)一些大型網(wǎng)站中應(yīng)用的非常普遍。
當(dāng)然一旦實(shí)現(xiàn)分庫分表后,將會牽扯到5個非常關(guān)鍵的問題,如下所示:
1、單機(jī)ACID被打破,分布式事務(wù)一致性難以保證;
2、數(shù)據(jù)查詢需要進(jìn)行跨庫跨表的數(shù)據(jù)路由;
3、多表之間的關(guān)聯(lián)查詢會有影響;
4、單庫中依賴于主鍵序列自增時生成的唯一ID會有影響;
5、強(qiáng)外鍵(外鍵約束)難以支持,僅能考慮弱外鍵(約定);
三、目前市面上常見的一些Sharding中間件產(chǎn)品對比
其實(shí)目前市面上的分庫分表產(chǎn)品不在少數(shù),但是這類產(chǎn)品,更多的是基于Proxy架構(gòu)的方式,在對于不看重通用性的前提下,基于應(yīng)用集成架構(gòu)的中間件則只剩下淘寶的TDDL和Mysql官方的Fabric。其實(shí)筆者還是非常喜歡TDDL的,Kratos中所支持的庫內(nèi)分片就是效仿的TDDL,相信大家也看得出來筆者對TDDL的感情。但是TDDL并非是絕對完美的,其弊端同樣明顯,比如:社區(qū)推進(jìn)力度緩慢、文檔資料匱乏、過多的功能、外圍系統(tǒng)依賴,再加上致命傷非開源,因此注定了TDDL無法為欣賞它的非淘寶系用戶服務(wù)。而Fabric,筆者接觸的太少了,并且正式版發(fā)行時間還是太短了,因此就不想當(dāng)小白鼠去試用,避免出現(xiàn)hold不住的情況。目前常見的一些Shardig中間件產(chǎn)品對比,如圖A-1所示:
圖A-1 常見的Shading中間件對比
在基于Proxy架構(gòu)的Sharding中間件中,大部分的產(chǎn)品基本上都是衍生子Cobar,并且這類產(chǎn)品對分片算法的支持都僅限于一庫一片的分片方式。對于庫內(nèi)分片和一庫一片等各自的優(yōu)缺點(diǎn),筆者稍后會進(jìn)行講解。具體使用什么樣的中間件產(chǎn)品,還需要根據(jù)具體的應(yīng)用場景而定,當(dāng)然如果是你正在愁找不到完善的使用手冊、配置手冊之類的文檔資料,Kratos你可以優(yōu)先考慮。
四、Kratos的架構(gòu)原型
簡單來說,分庫分表中間件無非就是根據(jù)Sharding算法對持有的多數(shù)據(jù)源進(jìn)行動態(tài)切換,這是任何Sharding中間件最核心的部分。一旦在程序中使用Kratos后,應(yīng)用層將會持有N個數(shù)據(jù)源,Kratos通過路由條件進(jìn)行運(yùn)算,然后通過Route技術(shù)對數(shù)據(jù)庫和數(shù)據(jù)表進(jìn)行讀寫操作。在此大家需要注意,Kratos內(nèi)部并沒有實(shí)現(xiàn)自己的ConnectionPool,這也就意味著,給了開發(fā)人員極大的自由,使之可以隨意切換任意的ConnectionPool產(chǎn)品,比如你覺得C3P0沒有BonePC性能高,那么你可以切換為BonePC。
對于開發(fā)人員而言,你并不需要關(guān)心底層的數(shù)據(jù)庫和表的劃分規(guī)則,程序中任何的CRUD操作,都像是在操作同一個數(shù)據(jù)庫一樣,并且讀寫效率還不能夠比之前低太多(比如幾毫秒或者實(shí)際毫秒之內(nèi)完成),而Kratos就承擔(dān)著這樣的一個任務(wù)。Kratos所處的領(lǐng)域模型定位,如圖A-2所示:
圖A-2 Kratos的領(lǐng)域模型定位
如圖A-2所示,Kratos的領(lǐng)域模型定位處于持久層和JDBC之間。之前筆者曾經(jīng)提及過,Kratos是站在巨人的肩膀上,這個巨人正是Spring。簡單來說,Kratos重寫了JdbcTemplate,并使用了Spring提供的AbstractRoutingDataSource作為動態(tài)數(shù)據(jù)源層。因此從另外一個側(cè)面反應(yīng)了Kratos的源碼注定是簡單、輕量、易閱讀、易維護(hù)的,因?yàn)镵ratos更多的關(guān)注點(diǎn)只會停留在Master/Slave讀寫分離層和分庫分表層。我們知道一般的Shading中間件,動不動就幾萬行代碼,其中得“貓膩”有很多,不僅數(shù)據(jù)庫連接池要自己寫、動態(tài)數(shù)據(jù)源要自己寫,再加上一些雜七雜八的功能,比如:通用性支持、多種類型的RDBMS或者Nosql支持,那么代碼自然冗余,可讀性極差。在Kratos中,這些都問題完全會“滾犢子”,因?yàn)镵ratos只會考慮如何通過Sharding規(guī)則實(shí)現(xiàn)數(shù)據(jù)路由。Kratos的3層架構(gòu),如圖A-3所示:
圖A-3 Kratos的3層架構(gòu)
既然Kratos只考慮最核心的功能,同時也就意味著它的性能恒定指標(biāo)還需要結(jié)合其他第三方產(chǎn)品,比如Kratos的動態(tài)數(shù)據(jù)源層所使用的ConnectionPool為C3P0,盡管非常穩(wěn)定的,但是性能遠(yuǎn)遠(yuǎn)比不上使用BonePC,因此大家完全可以將Kratos看做一個高效的黏合劑,它的核心任務(wù)就是數(shù)據(jù)路由,你別指望Kratos還能為你處理邊邊角角的零碎瑣事,想要什么效果,自行組合配置,這就是Kratos,一個簡單、輕量級的Sharding中間件。Kratos的應(yīng)用總體架構(gòu),如圖A-4所示:
圖A-4 Kratos的應(yīng)用總體架構(gòu)
五、動態(tài)數(shù)據(jù)源層的Master/Slave讀寫分離
當(dāng)大家對Kratos有了一個基本的了解后,那么接下來我們就來看看如何在程序中使用Kratos。com.gxl.kratos.jdbc.core.KratosJdbcTemplate是Kratos提供的一個Jdbc模板,它繼承自Spring的JdbcTemplate。簡單來說,KratosJdbcTemplate幾乎支持JdbcTemplate的所有方法(除批量操作外)。對于開發(fā)人員而言,只需要將JdbcTemplate替換為KratosJdbcTemplate即可,除此之外,程序中沒有其他任何需要進(jìn)行修改的地方,這種低侵入性相信大家都應(yīng)該能夠接受。
一般來說,數(shù)據(jù)庫的主從配置,既可以一主一從,也可以一主多從,但目前Kratos僅支持一主一從。接下來我們再來看看如何在配置文件中配置一主一從的讀寫分離操作,如下所示:
- <import resource="datasource1-context.xml" />
- <aop:aspectj-autoproxy proxy-target-class="true" />
- <context:component-scan base-package="com">
- <context:include-filter type="annotation"
- expression="org.aspectj.lang.annotation.Aspect" />
- </context:component-scan>
- <bean id="kJdbcTemplate" class="com.gxl.kratos.jdbc.core.KratosJdbcTemplate">
- <constructor-arg name="isShard" value="false"/>
- <property name="dataSource" ref="kDataSourceGroup"/>
- <property name="wr_weight" value="r1w0"/>
- </bean>
- <bean id="kDataSourceGroup" class="com.gxl.kratos.jdbc.datasource.config.KratosDatasourceGroup">
- <property name="targetDataSources">
- <map key-type="java.lang.Integer">
- <entry key="0" value-ref="dataSource1"/>
- <entry key="1" value-ref="dataSource2"/>
- </map>
- </property>
- </bean>
上述程序?qū)嵗校琧om.gxl.kratos.jdbc.datasource.config.KratosDatasourceGroup就是一個用于管理多數(shù)據(jù)源的Group,它繼承自Spring提供的AbstractRoutingDataSource,并充當(dāng)了動態(tài)數(shù)據(jù)源層的角色,由此基礎(chǔ)之上實(shí)現(xiàn)DBRoute。我們可以看見,在<entry/>標(biāo)簽中,key屬性指定了數(shù)據(jù)源索引,而value-ref屬性指定了數(shù)據(jù)源,通過這種簡單的鍵值對關(guān)系就可以明確的切換到具體的目標(biāo)數(shù)據(jù)源上。
在com.gxl.kratos.jdbc.core.KratosJdbcTemplate中,isShard屬性實(shí)際上就是一個Sharding開關(guān),缺省為false,也就意味著缺省是沒有開啟分庫分表的,那么在不Sharding的情況下,我們依然可以使用Kratos來完成讀寫分離操作。在wr_weight屬性中定義了讀寫分離的權(quán)重索引,也就是說,我們有多少個主庫,就一定需要有多少個從庫,比如主庫有1個,從庫也應(yīng)該是1個,因此KratosDatasourceGroup中持有的數(shù)據(jù)源個數(shù)就應(yīng)該一共是2個,索引從0-1,如果主庫的索引為0,那么從庫的索引就應(yīng)該為1,也就是“r1w0”。當(dāng)配置完成后,一旦Kratos監(jiān)測到執(zhí)行的操作為寫操作時,就會自動切換為主庫的數(shù)據(jù)源,而當(dāng)操作為讀操作的時候,就會自動切換為從庫的數(shù)據(jù)源,從而實(shí)現(xiàn)一主一從的讀寫分離操作。
六、Kratos的Sharding模型
目前市面上的Sharding中間的分庫分表算法有2種最常見的類型,分別是庫內(nèi)分片和一庫一片。筆者先從庫內(nèi)分片開始談起,并且會在后續(xù)進(jìn)行比較這2種分片算法的優(yōu)劣勢,讓大家更好的進(jìn)行選擇使用。
庫內(nèi)分片是TDDL采用的一種分片算法,這種分片算法相對來說較為復(fù)雜,因?yàn)椴粌H需要根據(jù)路由條件計算出數(shù)據(jù)應(yīng)該落盤到哪一個庫,還需要計算需要落盤到哪一個子表中。比如我們生產(chǎn)上有32個庫,數(shù)據(jù)庫表有1024張,那么平均每個庫中存放的子表數(shù)量就應(yīng)該是32個。庫內(nèi)分片算法示例,如圖A-5所示:
圖A-5 庫內(nèi)分片
一庫一片目前是非常常見的一種分片算法,它同時具備了簡單性和易維護(hù)性等特點(diǎn)。簡單來說,一旦通過路由算法計算出數(shù)據(jù)需要落盤到哪一個庫后,就等于間接的計算出了需要落盤到哪一個子表下。假設(shè)我們有1024個子表,那么對應(yīng)的數(shù)據(jù)庫也應(yīng)該是1024個,當(dāng)計算出數(shù)據(jù)需要落盤到第105個庫的時候,自然子表也就是第105個。一庫一片算法示例,如圖A-6所示:
圖A-6 一庫一片
那么我們究竟在生產(chǎn)中應(yīng)該選擇庫內(nèi)分片還是一庫一片呢?盡管Kratos這2種分片算法都同時支持,但生產(chǎn)上所使用的分片算法最好是統(tǒng)一的,千萬別一個子系統(tǒng)的分片算法使用的庫內(nèi)分片,而另外一個子系統(tǒng)使用的是一庫一片,因?yàn)檫@樣對于DBA來說將會極其痛苦,不僅維護(hù)極其困難,數(shù)據(jù)遷移也是一個頭痛的問題。筆者建議優(yōu)先考慮一庫一片這種分片方式,因?yàn)檫@種分片方式維護(hù)更簡單,并且在后期數(shù)據(jù)庫擴(kuò)容時,數(shù)據(jù)遷移工作更加容易和簡單,畢竟算出來庫就等于算出來表,遷移越簡單就意味著生產(chǎn)上停應(yīng)用的時間更短。當(dāng)然究竟應(yīng)該選擇哪一種分片算法,還需要你自行考慮抉擇。
七、Sharding之庫內(nèi)分片
之前筆者已經(jīng)提及過,Kratos的低侵入性設(shè)計只需要將原本的JdbcTemplate替換為KratosJdbcTemplate即可,除此之外,程序要不需要修改任何地方,因?yàn)樽x寫分離操作、分庫分表操作都僅僅只是在配置文件中進(jìn)行配置的,無需耦合在業(yè)務(wù)邏輯中。庫內(nèi)分片的配置,如下所示:
- <import resource="datasource1-context.xml" />
- <aop:aspectj-autoproxy proxy-target-class="true" />
- <!-- 自動掃描 -->
- <context:component-scan base-package="com">
- <context:include-filter type="annotation"
- expression="org.aspectj.lang.annotation.Aspect" />
- </context:component-scan>
- <bean id="kJdbcTemplate" class="com.gxl.kratos.jdbc.core.KratosJdbcTemplate">
- <!-- Sharding開關(guān) -->
- <constructor-arg name="isShard" value="true"/>
- <property name="dataSource" ref="kDataSourceGroup"/>
- <!--讀寫權(quán)重 -->
- <property name="wr_weight" value="r32w0"/>
- <!-- 分片算法模式,false為庫內(nèi)分片,true為一庫一片 -->
- <property name="shardMode" value="false"/>
- <!-- 分庫規(guī)則 -->
- <property name="dbRuleArray" value="#userinfo_id|email_hash# % 1024 / 32"/>
- <!-- 分表規(guī)則 -->
- <property name="tbRuleArray" value="#userinfo_id|email_hash# % 1024 % 32"/>
- </bean>
- <bean id="kDataSourceGroup" class="com.gxl.kratos.jdbc.datasource.config.KratosDatasourceGroup">
- <property name="targetDataSources">
- <map key-type="java.lang.Integer">
- <entry key="0" value-ref="dataSource1"/>
- <!-- 省略一部分?jǐn)?shù)據(jù)源... -->
- <entry key="63" value-ref="dataSource2"/>
- </map>
- </property>
- </bean>
上述程序示例中,筆者使用的是一主一從讀寫分離+庫內(nèi)分片模式。主庫一共是32個(1024個子表,每個庫包含子表數(shù)為32個),那么自然從庫也就是32個,在KratosDatasourceGroup中一共會持有64個數(shù)據(jù)源,數(shù)據(jù)源索引為0-63。那么在KratosJdbcTemplate中首先要做的事情是將分庫分片開關(guān)打開,然后讀寫權(quán)重索引wr_weight屬性的比例是“r32w0”,這也就意味著0-31都是主庫,而32-63都是從庫,Kratos會根據(jù)這個權(quán)重索引來自動根據(jù)所執(zhí)行的操作切換到對應(yīng)的主從數(shù)據(jù)源上。屬性shardMode其實(shí)就是指明了需要Kratos使用哪一種分片算法,true為一庫一片,而false則為庫內(nèi)分片。
屬性dbRuleArray指明了分庫規(guī)則,“#userinfo_id|email_hash# % 1024 / 32”指明了路由條件可能包括兩個,分別為userinfo_id和email_hash。然后根據(jù)路由條件先%tbSize,最后在/dbSize,即可計算出具體的數(shù)據(jù)源。在此大家需要注意,庫的倍數(shù)一定要是表的數(shù)量,否則數(shù)據(jù)將無法均勻分布到所有的子表上?;蛟S大家有個疑問,為什么路由條件會有多個呢?這是因?yàn)樵趯?shí)際的開發(fā)過程中,我們所有的查詢條件都需要根據(jù)路由條件來,并且實(shí)際情況不可能只有一個理由條件,甚至有可能更多(比如反向索引表)。因此通過符號“|”分隔開多個路由條件。當(dāng)一條sql執(zhí)行時,Kratos會匹配sql條件中的第一個數(shù)據(jù)庫參數(shù)字段是否是分庫分表條件,如果不是則會拋出異常com.gxl.kratos.jdbc.exception.ShardException。分表規(guī)則“#userinfo_id|email_hash# % 1024 % 32”其實(shí)大致和分庫規(guī)則一樣,只不過最后并非是/dbSize,而是%dbSize。經(jīng)過分庫分表算法后,一條sql就會被解析并落盤到指定的庫和指定的表中。
八、Sharding之一庫一片
一庫一片類似于庫內(nèi)分片的配置,但又細(xì)微的不同,之前筆者曾經(jīng)提及過,使用一庫一片算法后,根據(jù)路由條件計算出庫后,就等于間接計算出表,那么配置文件中就只需配置分庫規(guī)則即可。一庫一片的配置,如下所示:
- <import resource="datasource1-context.xml" />
- <aop:aspectj-autoproxy proxy-target-class="true" />
- <!-- 自動掃描 -->
- <context:component-scan base-package="com">
- <context:include-filter type="annotation"
- expression="org.aspectj.lang.annotation.Aspect" />
- </context:component-scan>
- <bean id="kJdbcTemplate" class="com.gxl.kratos.jdbc.core.KratosJdbcTemplate">
- <!-- Sharding開關(guān) -->
- <constructor-arg name="isShard" value="true"/>
- <property name="dataSource" ref="kDataSourceGroup"/>
- <!--讀寫權(quán)重 -->
- <property name="wr_weight" value="r32w0"/>
- <!-- 分片算法模式,false為庫內(nèi)分片,true為一庫一片 -->
- <property name="shardMode" value="true"/>
- <!-- 分庫規(guī)則 -->
- <property name="dbRuleArray" value="#userinfo_id|email_hash# % 32"/>
- </bean>
- <bean id="kDataSourceGroup" class="com.gxl.kratos.jdbc.datasource.config.KratosDatasourceGroup">
- <property name="targetDataSources">
- <map key-type="java.lang.Integer">
- <entry key="0" value-ref="dataSource1"/>
- <!-- 省略一部分?jǐn)?shù)據(jù)源... -->
- <entry key="63" value-ref="dataSource2"/>
- </map>
- </property>
- </bean>
上述程序示例中,筆者使用的是一主一從讀寫分離+一庫一片模式。主庫一共是32個(32個子表,每個庫包含子表數(shù)為1個),那么自然從庫也就是32個,在KratosDatasourceGroup中一共會持有64個數(shù)據(jù)源,數(shù)據(jù)源索引為0-63。權(quán)重索引wr_weight屬性的比例是“r32w0”,這也就意味著0-31都是主庫,而32-63都是從庫,Kratos會根據(jù)這個權(quán)重索引來自動根據(jù)所執(zhí)行的操作切換到對應(yīng)的主從數(shù)據(jù)源上。屬性shardMode其實(shí)就是指明了需要Kratos使用哪一種分片算法,true為一庫一片,而false則為庫內(nèi)分片。
屬性dbRuleArray指明了分庫規(guī)則,“#userinfo_id|email_hash# % 32”指明了路由條件可能包括兩個,分別為userinfo_id和email_hash。然后根據(jù)路由條件%dbSize即可計算出數(shù)據(jù)究竟應(yīng)該落盤到哪一個庫的哪一個子表下。
九、自動生成全局唯一的sequenceId
一旦我們分庫分表后,原本單庫中使用的序列自增Id將無法再繼續(xù)使用,那么這應(yīng)該怎么辦呢?其實(shí)解決這個問題并不困難,目前有2種方案,第一種是所有的應(yīng)用統(tǒng)一調(diào)用一個集中式的Id生成器,另外一種則是每個應(yīng)用集成一個Id生成器。無論選擇哪一種方案,Id生成器所持有的DB都只有一個,也就是說,通過一個通用DB去管理和生成全局以為的sequenceId。
目前市面上幾乎所有的Sharding中間件都沒有提供sequenceId的支持,而Kratos的工具包中卻為大家提供了支持。Kratos選擇的是每個應(yīng)用都集成一個Id生成器,而沒有采用集中式Id生成器,因?yàn)樵诜植际綀鼍跋拢嘁粋€外圍系統(tǒng)依賴就意味著多一分風(fēng)險,相信這個道理大家都應(yīng)該懂。那么究竟應(yīng)該如何使用Kratos提供的Id生成器呢?首先我們需要單獨(dú)準(zhǔn)備一個全局DB出來,然后使用Kratos的建表語句,如下所示:
- CREATE TABLE kratos_sequenceid(
- k_id INT NOT NULL AUTO_INCREMENT COMMENT '主鍵',
- k_type INT NOT NULL COMMENT '類型',
- k_useData BIGINT NOT NULL COMMENT '申請占位數(shù)量',
- PRIMARY KEY (k_id)
- )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
當(dāng)成功建立好生成sequenceId所需要的數(shù)據(jù)庫表后,接下來要做的事情就是進(jìn)行調(diào)用。生成sequenceId,如下所示:
- /**
- * 獲取SequenceId
- *
- * @author gaoxianglong
- */
- public @Test void getSequenceId() {
- /* 初始化數(shù)據(jù)源信息 */
- DbConnectionManager.init("account", "pwd", "url", "driver");
- System.out.println(SequenceIDManger.getSequenceId(1, 1, 5000));
- }
上述程序示例中,首先需要調(diào)用com.gxl.kratos.utils.sequence.DbConnectionManager類的init()方法對數(shù)據(jù)源進(jìn)行初始化,然后調(diào)用com.gxl.kratos.utils.sequence.DbConnectionManager類的getSequenceId()方法即可成功獲取全局唯一的sequenceId。在此大家需要注意,Kratos生成的sequenceId是一個17-19位之間的整型,在getSequenceId()方法中,第一個參數(shù)為IDC機(jī)房編碼,第二個參數(shù)為類型操作碼,而最后一個參數(shù)非常關(guān)鍵,就是需要向DB中申請的內(nèi)存占位數(shù)量(自增碼)。
簡單來說,相信大家都知道,既然業(yè)務(wù)庫都分庫分表了,就是為了緩解數(shù)據(jù)庫讀寫瓶頸,當(dāng)并發(fā)較高時,一個生成sequenceId的通用數(shù)據(jù)庫能扛得住嗎?筆者告訴你,扛得住!就是因?yàn)閮?nèi)存占位。其實(shí)原理很簡單,當(dāng)?shù)谝淮螒?yīng)用從Id生成器中去拿sequenceId時,Id生成器會鎖表并將數(shù)據(jù)庫字段k_useData更新為5000,那么第二次應(yīng)用從Id生成器中去拿sequenceId時,將不會與DB建議物理會話鏈接,而是直接在內(nèi)存中去消耗著5000內(nèi)存占位數(shù),直至消耗殆盡時,才會重新去數(shù)據(jù)庫中申請下一次的內(nèi)存占位。
那么問題又來了,如果并發(fā)訪問會不會有問題呢?其實(shí)保證全局唯一性有3點(diǎn),第一是程序中會加鎖,其次數(shù)據(jù)庫會for update,最后每一個操作碼都是唯一的,都會管理自己旗下的內(nèi)存占位數(shù)(通過Max()函數(shù)比較,累加下一個5000)?;蛟S你會在想,如何提升Id生成器的性能,盡可能的避免與數(shù)據(jù)庫建立物理會話,沒錯,這么想是對的,每次假設(shè)從數(shù)據(jù)庫申請的占位數(shù)量是50000,那么性能肯定比只申請5000好,但是這也有弊端,一旦程序出現(xiàn)down機(jī),內(nèi)存中的內(nèi)存數(shù)量就會丟失,只能重新申請,這會造成資源浪費(fèi)。
十、自動生成kratos分庫分表配置文件
Kratos的工具包中除了提供有自動生成sequenceId的功能外,還提供有自動生成分庫分表配置文件等功能。筆者自己是非常喜歡這個功能。因?yàn)檫@很明顯可以減少配置出錯率。比如我們采用庫內(nèi)分片模式,32個庫1024個表,數(shù)據(jù)源配置文件中,需要配置的數(shù)據(jù)源信息會有32個,當(dāng)然這通過手工的方式也未嘗不可,無非就是一個kratos分庫分表配置文件+一個dataSource文件(如果主從的話,還需要一個從庫的dataSource文件),dataSource文件中需要編寫32個數(shù)據(jù)源信息的<bean/>標(biāo)簽。但是如果我們使用的是一庫一片這種分片方式,使用的庫的數(shù)量是1024個的時候呢?dataSource文件中需要定義的數(shù)據(jù)源將會是1024個?寫不死你嗎?你能保證配置不會出問題?
既然手動編寫配置文件可能會出現(xiàn)錯誤,那么究竟應(yīng)該如何使用Kratos提供的自動生成配置文件功能來降低出錯率呢?Kratos自動生成配置文件,如下所示:
- /**
- * 生成核心配置文件
- *
- * @author gaoxianglong
- */
- public @Test void testCreateCoreXml() {
- CreateXml c_xml = new CreateXml();
- /* 是否控制臺輸出生成的配置文件 */
- c_xml.setIsShow(true);
- /* 配置分庫分片信息 */
- c_xml.setDbSize("1024");
- c_xml.setShard("true");
- c_xml.setWr_weight("r0w0");
- c_xml.setShardMode("false");
- c_xml.setDbRuleArray("#userinfo_id|email_hash# % 1024");
- //c_xml.setTbRuleArray("#userinfo_id|email_hash# % 1024 % 32");
- /* 執(zhí)行配置文件輸出 */
- System.out.println(c_xml.createCoreXml(new File("e:/kratos-context.xml")));
- }
- /**
- * 生成數(shù)據(jù)源配置文件
- *
- * @author gaoxianglong
- */
- public @Test void testCreateDadasourceXml() {
- CreateXml c_xml = new CreateXml();
- /* 是否控制臺輸出生成的配置文件 */
- c_xml.setIsShow(true);
- /* 數(shù)據(jù)源索引起始 */
- c_xml.setDataSourceIndex(1);
- /* 配置分庫分片信息 */
- c_xml.setDbSize("1024");
- /* 配置數(shù)據(jù)源信息 */
- c_xml.setJdbcUrl("jdbc:mysql://ip:3306/um");
- c_xml.setUser("${name}");
- c_xml.setPassword("${password}");
- c_xml.setDriverClass("${driverClass}");
- c_xml.setInitialPoolSize("${initialPoolSize}");
- c_xml.setMinPoolSize("${minPoolSize}");
- c_xml.setMaxPoolSize("${maxPoolSize}");
- c_xml.setMaxStatements("${maxStatements}");
- c_xml.setMaxIdleTime("${maxIdleTime}");
- /* 執(zhí)行配置文件輸出 */
- System.out.println(c_xml.createDatasourceXml(new File("e:/dataSource-context.xml")));
- }
- /**
- * 生成master/slave數(shù)據(jù)源配置文件
- *
- * @author gaoxianglong
- */
- public @Test void testCreateMSXml() {
- CreateXml c_xml = new CreateXml();
- c_xml.setIsShow(true);
- /* 生成master數(shù)據(jù)源信息 */
- c_xml.setDataSourceIndex(1);
- c_xml.setDbSize("32");
- c_xml.setJdbcUrl("jdbc:mysql://ip1:3306/um");
- c_xml.setUser("${name}");
- c_xml.setPassword("${password}");
- c_xml.setDriverClass("${driverClass}");
- c_xml.setInitialPoolSize("${initialPoolSize}");
- c_xml.setMinPoolSize("${minPoolSize}");
- c_xml.setMaxPoolSize("${maxPoolSize}");
- c_xml.setMaxStatements("${maxStatements}");
- c_xml.setMaxIdleTime("${maxIdleTime}");
- System.out.println(c_xml.createDatasourceXml(new File("e:/masterDataSource-context.xml")));
- /* 生成slave數(shù)據(jù)源信息 */
- c_xml.setDataSourceIndex(33);
- c_xml.setDbSize("32");
- c_xml.setJdbcUrl("jdbc:mysql://ip2:3306/um");
- c_xml.setUser("${name}");
- c_xml.setPassword("${password}");
- c_xml.setDriverClass("${driverClass}");
- c_xml.setInitialPoolSize("${initialPoolSize}");
- c_xml.setMinPoolSize("${minPoolSize}");
- c_xml.setMaxPoolSize("${maxPoolSize}");
- c_xml.setMaxStatements("${maxStatements}");
- c_xml.setMaxIdleTime("${maxIdleTime}");
- System.out.println(c_xml.createDatasourceXml(new File("e:/slaveDataSource-context.xml")));
- }
十一、注意事項
一旦在程序中使用Kratos進(jìn)行Sharding后,sql的編寫一定要注意,否則將無法進(jìn)行路由。sql規(guī)則如下所示:
1、暫時不支持分布式事物,因此無法保證事務(wù)一致性;
2、不支持多表查詢,所有多表查詢sql,務(wù)必全部打散為單條sql分開查詢;
3、不建議使用一些數(shù)據(jù)庫統(tǒng)計函數(shù)、Order by語句等;
4、sql的參數(shù)第一個必須是路由條件;
5、不支持?jǐn)?shù)據(jù)庫別名;
6、路由條件必須是整型;
7、子表后綴為符號"_"+4位整型,比如“tb_0001”——"tb_1024";
注意:
目前Kratos還處于1.2階段,未來還有很長的路要走,現(xiàn)在筆者測試環(huán)境上已經(jīng)在大規(guī)模的使用,后續(xù)生產(chǎn)環(huán)境上將會開始投放使用,因此如果各位在使用過程中有任何疑問,都可以通過企鵝群:150445731進(jìn)行交流,或者獲取Kratos的構(gòu)件。