《筆者帶你剖析輕量級(jí)Sharding中間件——Kratos1.x》
之所以編寫(xiě)Kratos其實(shí)存在一個(gè)小插曲,當(dāng)筆者滿山遍野尋找成熟、穩(wěn)定、高性能的Sharding中間件時(shí),確實(shí)是翻山越嶺,只不過(guò)始終沒(méi)有找到一款合適筆者項(xiàng)目場(chǎng)景的中間件產(chǎn)品。依稀記得當(dāng)年第一款使用的Sharding中間件就是淘寶的TDDL3.0,只可惜現(xiàn)在拿不到源碼。而其它的中間件,大多都是基于Proxy的,相信做過(guò)分布式系統(tǒng)的人都知道,拋開(kāi)網(wǎng)絡(luò)消耗所帶來(lái)的性能問(wèn)題不談,多一個(gè)外圍系統(tǒng)依賴(lài)就意味著需要多增加和承擔(dān)一份風(fēng)險(xiǎn),因此與應(yīng)用集成的方式,則成為筆者選擇的首要條件。當(dāng)然不是說(shuō)基于Proxy方式的Sharding中間件不好,只能說(shuō)我個(gè)人并不看重通用型需求,僅此而已。其實(shí)目前社區(qū)非?;钴S的MyCat,筆者在這里要批評(píng)一下,既然開(kāi)源,那么就請(qǐng)?jiān)贕itHub上貼上使用手冊(cè),而不是配置手冊(cè),因?yàn)閷?duì)于一個(gè)任何一個(gè)DVP而言,他要的是迅速上手的幫助文檔,而不是要使用你們的開(kāi)源產(chǎn)品還需要在淘寶上購(gòu)買(mǎi)幾十塊一本的網(wǎng)絡(luò)書(shū),對(duì)于這一點(diǎn),我非常鄙視和厭惡。
許多開(kāi)發(fā)人員動(dòng)不動(dòng)就大談分庫(kù)分表來(lái)顯示自己的成就感,那么筆者需要做的事情就是將其拉下神壇,讓大家切切實(shí)實(shí)感受到一款平民化技術(shù)所帶來(lái)的親民性。作為一款數(shù)據(jù)路由工具,Kratos采用與應(yīng)用集成的方式為開(kāi)發(fā)人員帶來(lái)超強(qiáng)的易用性。就目前而言,筆者測(cè)試環(huán)境上,物理環(huán)境選用為32庫(kù)和1024表的庫(kù)內(nèi)分片模式,因此筆者就先不必王婆賣(mài)瓜,自賣(mài)自夸其所謂的高性能和高穩(wěn)定。至于你是否能夠從Kratos中獲益,取決于你是否想用最簡(jiǎn)單,最易上手的方式來(lái)解決分庫(kù)分表場(chǎng)景下的數(shù)據(jù)路由工作。
筆者此篇博文,并不是宣導(dǎo)和普及分庫(kù)分表理論,而是作為Kratos貨真價(jià)實(shí)的Sharding權(quán)威指南手冊(cè)呈現(xiàn)給大家。不過(guò)實(shí)在抱歉,目前Kratos暫無(wú)開(kāi)源打算,只提供免費(fèi)試用,如果后期有開(kāi)源計(jì)劃,會(huì)在第一時(shí)間發(fā)布在GitHub上。
目錄
一、Kratos簡(jiǎn)介;
二、互聯(lián)網(wǎng)當(dāng)下的數(shù)據(jù)拆分過(guò)程;
三、目前市面上常見(jiàn)的一些Sharding中間件產(chǎn)品對(duì)比;
四、Kratos的架構(gòu)原型;
五、動(dòng)態(tài)數(shù)據(jù)源層的Master/Slave讀寫(xiě)分離;
六、Kratos的Sharding模型;
七、Sharding之庫(kù)內(nèi)分片;
八、Sharding之一庫(kù)一片;
九、自動(dòng)生成全局唯一的sequenceId;
十、自動(dòng)生成kratos分庫(kù)分表配置文件;
十一、注意事項(xiàng);
一、Kratos簡(jiǎn)介
因?yàn)檎也坏胶线m的Shading中間件,其次一些開(kāi)源的Shading中間件動(dòng)不動(dòng)就幾萬(wàn)行的代碼真有必要嗎?因此誕生了編寫(xiě)自己中間件的想發(fā)。Kratos這個(gè)名字來(lái)源于筆者之前在PS3平臺(tái)玩的一款A(yù)CT游戲《戰(zhàn)神3》中嗜血?dú)⑸竦闹鹘强鼱擪ratos,盡管筆者的Kratos并沒(méi)有展現(xiàn)出秒殺其他Sharding中間件的霸氣,但Kratos要做的事情很純粹,僅僅就只是處理分庫(kù)分表場(chǎng)景下的數(shù)據(jù)路由工作,它處于數(shù)據(jù)路由層,介于持久層與JDBC之間,因此沒(méi)有其它看似有用實(shí)則花哨的雞肋功能,并且與應(yīng)用層集成的方式注定了Kratos必然擁有更好的易用性。
對(duì)于開(kāi)發(fā)人員而言,在使用Kratos的時(shí)候,就好像是在操作同一個(gè)數(shù)據(jù)源一樣,也就是說(shuō),之前你的sql怎么寫(xiě),換上Kratos之后,業(yè)務(wù)邏輯無(wú)需做出變動(dòng),你只需要關(guān)心你的邏輯即可,數(shù)據(jù)路由工作,你完全可以放心的交由Kratos去處理。Kratos站在巨人的肩膀上,必然擁有更大的施展空間。首先Kratos依賴(lài)于Spring JDBC,那么如果你習(xí)慣使用JdbcTemplate,那就都不是問(wèn)題,反之你可以先看一下筆者的博文《筆者帶你剖析Spring3.x JDBC》。其次Kratos目前僅僅只支持Mysql數(shù)據(jù)庫(kù),對(duì)于其他RDBMS類(lèi)型的數(shù)據(jù)庫(kù),基本上以后也不會(huì)支持,簡(jiǎn)單來(lái)說(shuō),通用型需求并不是Kratos的目標(biāo),做好一件事才是真正的實(shí)惠。
kratos的優(yōu)點(diǎn):
1、動(dòng)態(tài)數(shù)據(jù)源的無(wú)縫切換;
2、master/slave一主一從讀寫(xiě)分離;
3、單線程讀重試(取決于的數(shù)據(jù)庫(kù)連接池是否支持);
4、單獨(dú)支持Mysql數(shù)據(jù)庫(kù);
5、非Proxy架構(gòu),與應(yīng)用集成,應(yīng)用直連數(shù)據(jù)庫(kù),降低外圍系統(tǒng)依賴(lài)帶來(lái)的down機(jī)風(fēng)險(xiǎn);
6、使用簡(jiǎn)單,侵入型低,站在巨人的肩膀上,依賴(lài)于Spring JDBC;
7、分庫(kù)分表路由算法支持2種分片模式,庫(kù)內(nèi)分片/一庫(kù)一片;
8、提供自動(dòng)生成sequenceId的API支持;
9、提供自動(dòng)生成配置文件的支持,降低配置出錯(cuò)率;
二、互聯(lián)網(wǎng)當(dāng)下的數(shù)據(jù)拆分過(guò)程
對(duì)于一個(gè)剛上線的互聯(lián)網(wǎng)項(xiàng)目來(lái)說(shuō),由于前期活躍度并不大,并發(fā)量相對(duì)較小,因此企業(yè)一般都會(huì)選擇將所有數(shù)據(jù)存放在一個(gè)物理DB中進(jìn)行讀寫(xiě)操作。但隨著后續(xù)的市場(chǎng)推廣力度不斷加強(qiáng),活躍度不斷提升,這時(shí)如果僅靠一個(gè)DB來(lái)支撐所有讀寫(xiě)壓力,就會(huì)顯得力不從心。所以一般到了這個(gè)階段,大部分Mysql DBA就會(huì)將數(shù)據(jù)庫(kù)設(shè)置為讀寫(xiě)分離狀態(tài)(一主一從/一主多從),Master負(fù)責(zé)寫(xiě)操作,而Slave負(fù)責(zé)讀操作。按照二八定律,80%的操作更多是讀操作,那么剩下的20%則為寫(xiě)操作,經(jīng)過(guò)讀寫(xiě)分離后,大大提升了單庫(kù)無(wú)法支撐的負(fù)載壓力。不過(guò)光靠讀寫(xiě)分離并不會(huì)一勞永逸,如果活躍度再次提升,相信又會(huì)再次遇見(jiàn)數(shù)據(jù)庫(kù)的讀寫(xiě)瓶頸。因此到了這個(gè)階段,就需要實(shí)現(xiàn)垂直分庫(kù)。
所謂垂直分庫(kù),簡(jiǎn)單來(lái)說(shuō)就是根據(jù)業(yè)務(wù)的不同將原本冗余在單庫(kù)中的業(yè)務(wù)表拆散,分布到不同的業(yè)務(wù)庫(kù)中,實(shí)現(xiàn)分而治之的讀寫(xiě)訪問(wèn)操作。當(dāng)然我們都知道,傳統(tǒng)RDBMS數(shù)據(jù)庫(kù)中的數(shù)據(jù)表隨著數(shù)據(jù)量的暴增,從維護(hù)性和高響應(yīng)的角度去看,無(wú)論任何CRUD操作,對(duì)于數(shù)據(jù)庫(kù)而言,都是一件極其傷腦筋的事情。即便設(shè)置了索引,檢索效率依然低下,因?yàn)殡S著數(shù)據(jù)量的暴增,RDBMS數(shù)據(jù)庫(kù)的性能瓶頸就會(huì)逐漸顯露出來(lái)。這一點(diǎn),Nosql數(shù)據(jù)庫(kù)倒是做得很好,當(dāng)然架構(gòu)不同,所以不予比較。那么既然已經(jīng)到了這個(gè)節(jié)骨眼上了,唯一的殺手锏就是在垂直分庫(kù)的基礎(chǔ)之上進(jìn)行水平分區(qū),也就是我們常說(shuō)的Sharding。
簡(jiǎn)單來(lái)說(shuō),水平分區(qū)要做的事情,就是將原本冗余在單庫(kù)中的業(yè)務(wù)表分散為N個(gè)子表(比如tab_0001、tab_0002、tab_0003、tab_0004...)分別存放在不同的子庫(kù)中。理論上來(lái)講,子表之間通過(guò)某種契約關(guān)聯(lián)在一起,每一張子表均按段位進(jìn)行數(shù)據(jù)存儲(chǔ),比如tab_0000存儲(chǔ)1-10000的數(shù)據(jù),而tab_0001存儲(chǔ)10001-20000的數(shù)據(jù)。經(jīng)過(guò)水平分區(qū)后,必然能夠?qū)⒃居梢粡垬I(yè)務(wù)表維護(hù)的海量數(shù)據(jù)分配給N個(gè)子表進(jìn)行讀寫(xiě)操作和維護(hù),大大提升了數(shù)據(jù)庫(kù)的讀寫(xiě)性能,降低了性能瓶頸?;诜謳?kù)分表的設(shè)計(jì),目前在國(guó)內(nèi)一些大型網(wǎng)站中應(yīng)用的非常普遍。
當(dāng)然一旦實(shí)現(xiàn)分庫(kù)分表后,將會(huì)牽扯到5個(gè)非常關(guān)鍵的問(wèn)題,如下所示:
1、單機(jī)ACID被打破,分布式事務(wù)一致性難以保證;
2、數(shù)據(jù)查詢需要進(jìn)行跨庫(kù)跨表的數(shù)據(jù)路由;
3、多表之間的關(guān)聯(lián)查詢會(huì)有影響;
4、單庫(kù)中依賴(lài)于主鍵序列自增時(shí)生成的唯一ID會(huì)有影響;
5、強(qiáng)外鍵(外鍵約束)難以支持,僅能考慮弱外鍵(約定);
三、目前市面上常見(jiàn)的一些Sharding中間件產(chǎn)品對(duì)比
其實(shí)目前市面上的分庫(kù)分表產(chǎn)品不在少數(shù),但是這類(lèi)產(chǎn)品,更多的是基于Proxy架構(gòu)的方式,在對(duì)于不看重通用性的前提下,基于應(yīng)用集成架構(gòu)的中間件則只剩下淘寶的TDDL和Mysql官方的Fabric。其實(shí)筆者還是非常喜歡TDDL的,Kratos中所支持的庫(kù)內(nèi)分片就是效仿的TDDL,相信大家也看得出來(lái)筆者對(duì)TDDL的感情。但是TDDL并非是絕對(duì)完美的,其弊端同樣明顯,比如:社區(qū)推進(jìn)力度緩慢、文檔資料匱乏、過(guò)多的功能、外圍系統(tǒng)依賴(lài),再加上致命傷非開(kāi)源,因此注定了TDDL無(wú)法為欣賞它的非淘寶系用戶服務(wù)。而Fabric,筆者接觸的太少了,并且正式版發(fā)行時(shí)間還是太短了,因此就不想當(dāng)小白鼠去試用,避免出現(xiàn)hold不住的情況。目前常見(jiàn)的一些Shardig中間件產(chǎn)品對(duì)比,如圖A-1所示:

圖A-1 常見(jiàn)的Shading中間件對(duì)比
在基于Proxy架構(gòu)的Sharding中間件中,大部分的產(chǎn)品基本上都是衍生子Cobar,并且這類(lèi)產(chǎn)品對(duì)分片算法的支持都僅限于一庫(kù)一片的分片方式。對(duì)于庫(kù)內(nèi)分片和一庫(kù)一片等各自的優(yōu)缺點(diǎn),筆者稍后會(huì)進(jìn)行講解。具體使用什么樣的中間件產(chǎn)品,還需要根據(jù)具體的應(yīng)用場(chǎng)景而定,當(dāng)然如果是你正在愁找不到完善的使用手冊(cè)、配置手冊(cè)之類(lèi)的文檔資料,Kratos你可以優(yōu)先考慮。
四、Kratos的架構(gòu)原型
簡(jiǎn)單來(lái)說(shuō),分庫(kù)分表中間件無(wú)非就是根據(jù)Sharding算法對(duì)持有的多數(shù)據(jù)源進(jìn)行動(dòng)態(tài)切換,這是任何Sharding中間件最核心的部分。一旦在程序中使用Kratos后,應(yīng)用層將會(huì)持有N個(gè)數(shù)據(jù)源,Kratos通過(guò)路由條件進(jìn)行運(yùn)算,然后通過(guò)Route技術(shù)對(duì)數(shù)據(jù)庫(kù)和數(shù)據(jù)表進(jìn)行讀寫(xiě)操作。在此大家需要注意,Kratos內(nèi)部并沒(méi)有實(shí)現(xiàn)自己的ConnectionPool,這也就意味著,給了開(kāi)發(fā)人員極大的自由,使之可以隨意切換任意的ConnectionPool產(chǎn)品,比如你覺(jué)得C3P0沒(méi)有BonePC性能高,那么你可以切換為BonePC。
對(duì)于開(kāi)發(fā)人員而言,你并不需要關(guān)心底層的數(shù)據(jù)庫(kù)和表的劃分規(guī)則,程序中任何的CRUD操作,都像是在操作同一個(gè)數(shù)據(jù)庫(kù)一樣,并且讀寫(xiě)效率還不能夠比之前低太多(比如幾毫秒或者實(shí)際毫秒之內(nèi)完成),而Kratos就承擔(dān)著這樣的一個(gè)任務(wù)。Kratos所處的領(lǐng)域模型定位,如圖A-2所示:

圖A-2 Kratos的領(lǐng)域模型定位
如圖A-2所示,Kratos的領(lǐng)域模型定位處于持久層和JDBC之間。之前筆者曾經(jīng)提及過(guò),Kratos是站在巨人的肩膀上,這個(gè)巨人正是Spring。簡(jiǎn)單來(lái)說(shuō),Kratos重寫(xiě)了JdbcTemplate,并使用了Spring提供的AbstractRoutingDataSource作為動(dòng)態(tài)數(shù)據(jù)源層。因此從另外一個(gè)側(cè)面反應(yīng)了Kratos的源碼注定是簡(jiǎn)單、輕量、易閱讀、易維護(hù)的,因?yàn)镵ratos更多的關(guān)注點(diǎn)只會(huì)停留在Master/Slave讀寫(xiě)分離層和分庫(kù)分表層。我們知道一般的Shading中間件,動(dòng)不動(dòng)就幾萬(wàn)行代碼,其中得“貓膩”有很多,不僅數(shù)據(jù)庫(kù)連接池要自己寫(xiě)、動(dòng)態(tài)數(shù)據(jù)源要自己寫(xiě),再加上一些雜七雜八的功能,比如:通用性支持、多種類(lèi)型的RDBMS或者Nosql支持,那么代碼自然冗余,可讀性極差。在Kratos中,這些都問(wèn)題完全會(huì)“滾犢子”,因?yàn)镵ratos只會(huì)考慮如何通過(guò)Sharding規(guī)則實(shí)現(xiàn)數(shù)據(jù)路由。Kratos的3層架構(gòu),如圖A-3所示:

圖A-3 Kratos的3層架構(gòu)
既然Kratos只考慮最核心的功能,同時(shí)也就意味著它的性能恒定指標(biāo)還需要結(jié)合其他第三方產(chǎn)品,比如Kratos的動(dòng)態(tài)數(shù)據(jù)源層所使用的ConnectionPool為C3P0,盡管非常穩(wěn)定的,但是性能遠(yuǎn)遠(yuǎn)比不上使用BonePC,因此大家完全可以將Kratos看做一個(gè)高效的黏合劑,它的核心任務(wù)就是數(shù)據(jù)路由,你別指望Kratos還能為你處理邊邊角角的零碎瑣事,想要什么效果,自行組合配置,這就是Kratos,一個(gè)簡(jiǎn)單、輕量級(jí)的Sharding中間件。Kratos的應(yīng)用總體架構(gòu),如圖A-4所示:

圖A-4 Kratos的應(yīng)用總體架構(gòu)
五、動(dòng)態(tài)數(shù)據(jù)源層的Master/Slave讀寫(xiě)分離
當(dāng)大家對(duì)Kratos有了一個(gè)基本的了解后,那么接下來(lái)我們就來(lái)看看如何在程序中使用Kratos。com.gxl.kratos.jdbc.core.KratosJdbcTemplate是Kratos提供的一個(gè)Jdbc模板,它繼承自Spring的JdbcTemplate。簡(jiǎn)單來(lái)說(shuō),KratosJdbcTemplate幾乎支持JdbcTemplate的所有方法(除批量操作外)。對(duì)于開(kāi)發(fā)人員而言,只需要將JdbcTemplate替換為KratosJdbcTemplate即可,除此之外,程序中沒(méi)有其他任何需要進(jìn)行修改的地方,這種低侵入性相信大家都應(yīng)該能夠接受。
一般來(lái)說(shuō),數(shù)據(jù)庫(kù)的主從配置,既可以一主一從,也可以一主多從,但目前Kratos僅支持一主一從。接下來(lái)我們?cè)賮?lái)看看如何在配置文件中配置一主一從的讀寫(xiě)分離操作,如下所示:
- <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就是一個(gè)用于管理多數(shù)據(jù)源的Group,它繼承自Spring提供的AbstractRoutingDataSource,并充當(dāng)了動(dòng)態(tài)數(shù)據(jù)源層的角色,由此基礎(chǔ)之上實(shí)現(xiàn)DBRoute。我們可以看見(jiàn),在<entry/>標(biāo)簽中,key屬性指定了數(shù)據(jù)源索引,而value-ref屬性指定了數(shù)據(jù)源,通過(guò)這種簡(jiǎn)單的鍵值對(duì)關(guān)系就可以明確的切換到具體的目標(biāo)數(shù)據(jù)源上。
在com.gxl.kratos.jdbc.core.KratosJdbcTemplate中,isShard屬性實(shí)際上就是一個(gè)Sharding開(kāi)關(guān),缺省為false,也就意味著缺省是沒(méi)有開(kāi)啟分庫(kù)分表的,那么在不Sharding的情況下,我們依然可以使用Kratos來(lái)完成讀寫(xiě)分離操作。在wr_weight屬性中定義了讀寫(xiě)分離的權(quán)重索引,也就是說(shuō),我們有多少個(gè)主庫(kù),就一定需要有多少個(gè)從庫(kù),比如主庫(kù)有1個(gè),從庫(kù)也應(yīng)該是1個(gè),因此KratosDatasourceGroup中持有的數(shù)據(jù)源個(gè)數(shù)就應(yīng)該一共是2個(gè),索引從0-1,如果主庫(kù)的索引為0,那么從庫(kù)的索引就應(yīng)該為1,也就是“r1w0”。當(dāng)配置完成后,一旦Kratos監(jiān)測(cè)到執(zhí)行的操作為寫(xiě)操作時(shí),就會(huì)自動(dòng)切換為主庫(kù)的數(shù)據(jù)源,而當(dāng)操作為讀操作的時(shí)候,就會(huì)自動(dòng)切換為從庫(kù)的數(shù)據(jù)源,從而實(shí)現(xiàn)一主一從的讀寫(xiě)分離操作。
六、Kratos的Sharding模型
目前市面上的Sharding中間的分庫(kù)分表算法有2種最常見(jiàn)的類(lèi)型,分別是庫(kù)內(nèi)分片和一庫(kù)一片。筆者先從庫(kù)內(nèi)分片開(kāi)始談起,并且會(huì)在后續(xù)進(jìn)行比較這2種分片算法的優(yōu)劣勢(shì),讓大家更好的進(jìn)行選擇使用。
庫(kù)內(nèi)分片是TDDL采用的一種分片算法,這種分片算法相對(duì)來(lái)說(shuō)較為復(fù)雜,因?yàn)椴粌H需要根據(jù)路由條件計(jì)算出數(shù)據(jù)應(yīng)該落盤(pán)到哪一個(gè)庫(kù),還需要計(jì)算需要落盤(pán)到哪一個(gè)子表中。比如我們生產(chǎn)上有32個(gè)庫(kù),數(shù)據(jù)庫(kù)表有1024張,那么平均每個(gè)庫(kù)中存放的子表數(shù)量就應(yīng)該是32個(gè)。庫(kù)內(nèi)分片算法示例,如圖A-5所示:

圖A-5 庫(kù)內(nèi)分片
一庫(kù)一片目前是非常常見(jiàn)的一種分片算法,它同時(shí)具備了簡(jiǎn)單性和易維護(hù)性等特點(diǎn)。簡(jiǎn)單來(lái)說(shuō),一旦通過(guò)路由算法計(jì)算出數(shù)據(jù)需要落盤(pán)到哪一個(gè)庫(kù)后,就等于間接的計(jì)算出了需要落盤(pán)到哪一個(gè)子表下。假設(shè)我們有1024個(gè)子表,那么對(duì)應(yīng)的數(shù)據(jù)庫(kù)也應(yīng)該是1024個(gè),當(dāng)計(jì)算出數(shù)據(jù)需要落盤(pán)到第105個(gè)庫(kù)的時(shí)候,自然子表也就是第105個(gè)。一庫(kù)一片算法示例,如圖A-6所示:

圖A-6 一庫(kù)一片
那么我們究竟在生產(chǎn)中應(yīng)該選擇庫(kù)內(nèi)分片還是一庫(kù)一片呢?盡管Kratos這2種分片算法都同時(shí)支持,但生產(chǎn)上所使用的分片算法最好是統(tǒng)一的,千萬(wàn)別一個(gè)子系統(tǒng)的分片算法使用的庫(kù)內(nèi)分片,而另外一個(gè)子系統(tǒng)使用的是一庫(kù)一片,因?yàn)檫@樣對(duì)于DBA來(lái)說(shuō)將會(huì)極其痛苦,不僅維護(hù)極其困難,數(shù)據(jù)遷移也是一個(gè)頭痛的問(wèn)題。筆者建議優(yōu)先考慮一庫(kù)一片這種分片方式,因?yàn)檫@種分片方式維護(hù)更簡(jiǎn)單,并且在后期數(shù)據(jù)庫(kù)擴(kuò)容時(shí),數(shù)據(jù)遷移工作更加容易和簡(jiǎn)單,畢竟算出來(lái)庫(kù)就等于算出來(lái)表,遷移越簡(jiǎn)單就意味著生產(chǎn)上停應(yīng)用的時(shí)間更短。當(dāng)然究竟應(yīng)該選擇哪一種分片算法,還需要你自行考慮抉擇。
七、Sharding之庫(kù)內(nèi)分片
之前筆者已經(jīng)提及過(guò),Kratos的低侵入性設(shè)計(jì)只需要將原本的JdbcTemplate替換為KratosJdbcTemplate即可,除此之外,程序要不需要修改任何地方,因?yàn)樽x寫(xiě)分離操作、分庫(kù)分表操作都僅僅只是在配置文件中進(jìn)行配置的,無(wú)需耦合在業(yè)務(wù)邏輯中。庫(kù)內(nèi)分片的配置,如下所示:
- <import resource="datasource1-context.xml" />
- <aop:aspectj-autoproxy proxy-target-class="true" />
- <!-- 自動(dòng)掃描 -->
- <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開(kāi)關(guān) -->
- <constructor-arg name="isShard" value="true"/>
- <property name="dataSource" ref="kDataSourceGroup"/>
- <!--讀寫(xiě)權(quán)重 -->
- <property name="wr_weight" value="r32w0"/>
- <!-- 分片算法模式,false為庫(kù)內(nèi)分片,true為一庫(kù)一片 -->
- <property name="shardMode" value="false"/>
- <!-- 分庫(kù)規(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>
上述程序示例中,筆者使用的是一主一從讀寫(xiě)分離+庫(kù)內(nèi)分片模式。主庫(kù)一共是32個(gè)(1024個(gè)子表,每個(gè)庫(kù)包含子表數(shù)為32個(gè)),那么自然從庫(kù)也就是32個(gè),在KratosDatasourceGroup中一共會(huì)持有64個(gè)數(shù)據(jù)源,數(shù)據(jù)源索引為0-63。那么在KratosJdbcTemplate中首先要做的事情是將分庫(kù)分片開(kāi)關(guān)打開(kāi),然后讀寫(xiě)權(quán)重索引wr_weight屬性的比例是“r32w0”,這也就意味著0-31都是主庫(kù),而32-63都是從庫(kù),Kratos會(huì)根據(jù)這個(gè)權(quán)重索引來(lái)自動(dòng)根據(jù)所執(zhí)行的操作切換到對(duì)應(yīng)的主從數(shù)據(jù)源上。屬性shardMode其實(shí)就是指明了需要Kratos使用哪一種分片算法,true為一庫(kù)一片,而false則為庫(kù)內(nèi)分片。
屬性dbRuleArray指明了分庫(kù)規(guī)則,“#userinfo_id|email_hash# % 1024 / 32”指明了路由條件可能包括兩個(gè),分別為userinfo_id和email_hash。然后根據(jù)路由條件先%tbSize,最后在/dbSize,即可計(jì)算出具體的數(shù)據(jù)源。在此大家需要注意,庫(kù)的倍數(shù)一定要是表的數(shù)量,否則數(shù)據(jù)將無(wú)法均勻分布到所有的子表上。或許大家有個(gè)疑問(wèn),為什么路由條件會(huì)有多個(gè)呢?這是因?yàn)樵趯?shí)際的開(kāi)發(fā)過(guò)程中,我們所有的查詢條件都需要根據(jù)路由條件來(lái),并且實(shí)際情況不可能只有一個(gè)理由條件,甚至有可能更多(比如反向索引表)。因此通過(guò)符號(hào)“|”分隔開(kāi)多個(gè)路由條件。當(dāng)一條sql執(zhí)行時(shí),Kratos會(huì)匹配sql條件中的第一個(gè)數(shù)據(jù)庫(kù)參數(shù)字段是否是分庫(kù)分表?xiàng)l件,如果不是則會(huì)拋出異常com.gxl.kratos.jdbc.exception.ShardException。分表規(guī)則“#userinfo_id|email_hash# % 1024 % 32”其實(shí)大致和分庫(kù)規(guī)則一樣,只不過(guò)最后并非是/dbSize,而是%dbSize。經(jīng)過(guò)分庫(kù)分表算法后,一條sql就會(huì)被解析并落盤(pán)到指定的庫(kù)和指定的表中。
八、Sharding之一庫(kù)一片
一庫(kù)一片類(lèi)似于庫(kù)內(nèi)分片的配置,但又細(xì)微的不同,之前筆者曾經(jīng)提及過(guò),使用一庫(kù)一片算法后,根據(jù)路由條件計(jì)算出庫(kù)后,就等于間接計(jì)算出表,那么配置文件中就只需配置分庫(kù)規(guī)則即可。一庫(kù)一片的配置,如下所示:
- <import resource="datasource1-context.xml" />
- <aop:aspectj-autoproxy proxy-target-class="true" />
- <!-- 自動(dòng)掃描 -->
- <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開(kāi)關(guān) -->
- <constructor-arg name="isShard" value="true"/>
- <property name="dataSource" ref="kDataSourceGroup"/>
- <!--讀寫(xiě)權(quán)重 -->
- <property name="wr_weight" value="r32w0"/>
- <!-- 分片算法模式,false為庫(kù)內(nèi)分片,true為一庫(kù)一片 -->
- <property name="shardMode" value="true"/>
- <!-- 分庫(kù)規(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>
上述程序示例中,筆者使用的是一主一從讀寫(xiě)分離+一庫(kù)一片模式。主庫(kù)一共是32個(gè)(32個(gè)子表,每個(gè)庫(kù)包含子表數(shù)為1個(gè)),那么自然從庫(kù)也就是32個(gè),在KratosDatasourceGroup中一共會(huì)持有64個(gè)數(shù)據(jù)源,數(shù)據(jù)源索引為0-63。權(quán)重索引wr_weight屬性的比例是“r32w0”,這也就意味著0-31都是主庫(kù),而32-63都是從庫(kù),Kratos會(huì)根據(jù)這個(gè)權(quán)重索引來(lái)自動(dòng)根據(jù)所執(zhí)行的操作切換到對(duì)應(yīng)的主從數(shù)據(jù)源上。屬性shardMode其實(shí)就是指明了需要Kratos使用哪一種分片算法,true為一庫(kù)一片,而false則為庫(kù)內(nèi)分片。
屬性dbRuleArray指明了分庫(kù)規(guī)則,“#userinfo_id|email_hash# % 32”指明了路由條件可能包括兩個(gè),分別為userinfo_id和email_hash。然后根據(jù)路由條件%dbSize即可計(jì)算出數(shù)據(jù)究竟應(yīng)該落盤(pán)到哪一個(gè)庫(kù)的哪一個(gè)子表下。
九、自動(dòng)生成全局唯一的sequenceId
一旦我們分庫(kù)分表后,原本單庫(kù)中使用的序列自增Id將無(wú)法再繼續(xù)使用,那么這應(yīng)該怎么辦呢?其實(shí)解決這個(gè)問(wèn)題并不困難,目前有2種方案,第一種是所有的應(yīng)用統(tǒng)一調(diào)用一個(gè)集中式的Id生成器,另外一種則是每個(gè)應(yīng)用集成一個(gè)Id生成器。無(wú)論選擇哪一種方案,Id生成器所持有的DB都只有一個(gè),也就是說(shuō),通過(guò)一個(gè)通用DB去管理和生成全局以為的sequenceId。
目前市面上幾乎所有的Sharding中間件都沒(méi)有提供sequenceId的支持,而Kratos的工具包中卻為大家提供了支持。Kratos選擇的是每個(gè)應(yīng)用都集成一個(gè)Id生成器,而沒(méi)有采用集中式Id生成器,因?yàn)樵诜植际綀?chǎng)景下,多一個(gè)外圍系統(tǒng)依賴(lài)就意味著多一分風(fēng)險(xiǎn),相信這個(gè)道理大家都應(yīng)該懂。那么究竟應(yīng)該如何使用Kratos提供的Id生成器呢?首先我們需要單獨(dú)準(zhǔn)備一個(gè)全局DB出來(lái),然后使用Kratos的建表語(yǔ)句,如下所示:
- CREATE TABLE kratos_sequenceid(
- k_id INT NOT NULL AUTO_INCREMENT COMMENT '主鍵',
- k_type INT NOT NULL COMMENT '類(lèi)型',
- k_useData BIGINT NOT NULL COMMENT '申請(qǐng)占位數(shù)量',
- PRIMARY KEY (k_id)
- )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;
當(dāng)成功建立好生成sequenceId所需要的數(shù)據(jù)庫(kù)表后,接下來(lái)要做的事情就是進(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類(lèi)的init()方法對(duì)數(shù)據(jù)源進(jìn)行初始化,然后調(diào)用com.gxl.kratos.utils.sequence.DbConnectionManager類(lèi)的getSequenceId()方法即可成功獲取全局唯一的sequenceId。在此大家需要注意,Kratos生成的sequenceId是一個(gè)17-19位之間的整型,在getSequenceId()方法中,第一個(gè)參數(shù)為IDC機(jī)房編碼,第二個(gè)參數(shù)為類(lèi)型操作碼,而最后一個(gè)參數(shù)非常關(guān)鍵,就是需要向DB中申請(qǐng)的內(nèi)存占位數(shù)量(自增碼)。
簡(jiǎn)單來(lái)說(shuō),相信大家都知道,既然業(yè)務(wù)庫(kù)都分庫(kù)分表了,就是為了緩解數(shù)據(jù)庫(kù)讀寫(xiě)瓶頸,當(dāng)并發(fā)較高時(shí),一個(gè)生成sequenceId的通用數(shù)據(jù)庫(kù)能扛得住嗎?筆者告訴你,扛得住!就是因?yàn)閮?nèi)存占位。其實(shí)原理很簡(jiǎn)單,當(dāng)?shù)谝淮螒?yīng)用從Id生成器中去拿sequenceId時(shí),Id生成器會(huì)鎖表并將數(shù)據(jù)庫(kù)字段k_useData更新為5000,那么第二次應(yīng)用從Id生成器中去拿sequenceId時(shí),將不會(huì)與DB建議物理會(huì)話鏈接,而是直接在內(nèi)存中去消耗著5000內(nèi)存占位數(shù),直至消耗殆盡時(shí),才會(huì)重新去數(shù)據(jù)庫(kù)中申請(qǐng)下一次的內(nèi)存占位。
那么問(wèn)題又來(lái)了,如果并發(fā)訪問(wèn)會(huì)不會(huì)有問(wèn)題呢?其實(shí)保證全局唯一性有3點(diǎn),第一是程序中會(huì)加鎖,其次數(shù)據(jù)庫(kù)會(huì)for update,最后每一個(gè)操作碼都是唯一的,都會(huì)管理自己旗下的內(nèi)存占位數(shù)(通過(guò)Max()函數(shù)比較,累加下一個(gè)5000)?;蛟S你會(huì)在想,如何提升Id生成器的性能,盡可能的避免與數(shù)據(jù)庫(kù)建立物理會(huì)話,沒(méi)錯(cuò),這么想是對(duì)的,每次假設(shè)從數(shù)據(jù)庫(kù)申請(qǐng)的占位數(shù)量是50000,那么性能肯定比只申請(qǐng)5000好,但是這也有弊端,一旦程序出現(xiàn)down機(jī),內(nèi)存中的內(nèi)存數(shù)量就會(huì)丟失,只能重新申請(qǐng),這會(huì)造成資源浪費(fèi)。
十、自動(dòng)生成kratos分庫(kù)分表配置文件
Kratos的工具包中除了提供有自動(dòng)生成sequenceId的功能外,還提供有自動(dòng)生成分庫(kù)分表配置文件等功能。筆者自己是非常喜歡這個(gè)功能。因?yàn)檫@很明顯可以減少配置出錯(cuò)率。比如我們采用庫(kù)內(nèi)分片模式,32個(gè)庫(kù)1024個(gè)表,數(shù)據(jù)源配置文件中,需要配置的數(shù)據(jù)源信息會(huì)有32個(gè),當(dāng)然這通過(guò)手工的方式也未嘗不可,無(wú)非就是一個(gè)kratos分庫(kù)分表配置文件+一個(gè)dataSource文件(如果主從的話,還需要一個(gè)從庫(kù)的dataSource文件),dataSource文件中需要編寫(xiě)32個(gè)數(shù)據(jù)源信息的<bean/>標(biāo)簽。但是如果我們使用的是一庫(kù)一片這種分片方式,使用的庫(kù)的數(shù)量是1024個(gè)的時(shí)候呢?dataSource文件中需要定義的數(shù)據(jù)源將會(huì)是1024個(gè)?寫(xiě)不死你嗎?你能保證配置不會(huì)出問(wèn)題?
既然手動(dòng)編寫(xiě)配置文件可能會(huì)出現(xiàn)錯(cuò)誤,那么究竟應(yīng)該如何使用Kratos提供的自動(dòng)生成配置文件功能來(lái)降低出錯(cuò)率呢?Kratos自動(dòng)生成配置文件,如下所示:
- /**
- * 生成核心配置文件
- *
- * @author gaoxianglong
- */
- public @Test void testCreateCoreXml() {
- CreateXml c_xml = new CreateXml();
- /* 是否控制臺(tái)輸出生成的配置文件 */
- c_xml.setIsShow(true);
- /* 配置分庫(kù)分片信息 */
- 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();
- /* 是否控制臺(tái)輸出生成的配置文件 */
- c_xml.setIsShow(true);
- /* 數(shù)據(jù)源索引起始 */
- c_xml.setDataSourceIndex(1);
- /* 配置分庫(kù)分片信息 */
- 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")));
- }
十一、注意事項(xiàng)
一旦在程序中使用Kratos進(jìn)行Sharding后,sql的編寫(xiě)一定要注意,否則將無(wú)法進(jìn)行路由。sql規(guī)則如下所示:
1、暫時(shí)不支持分布式事物,因此無(wú)法保證事務(wù)一致性;
2、不支持多表查詢,所有多表查詢sql,務(wù)必全部打散為單條sql分開(kāi)查詢;
3、不建議使用一些數(shù)據(jù)庫(kù)統(tǒng)計(jì)函數(shù)、Order by語(yǔ)句等;
4、sql的參數(shù)第一個(gè)必須是路由條件;
5、不支持?jǐn)?shù)據(jù)庫(kù)別名;
6、路由條件必須是整型;
7、子表后綴為符號(hào)"_"+4位整型,比如“tb_0001”——"tb_1024";
注意:
目前Kratos還處于1.2階段,未來(lái)還有很長(zhǎng)的路要走,現(xiàn)在筆者測(cè)試環(huán)境上已經(jīng)在大規(guī)模的使用,后續(xù)生產(chǎn)環(huán)境上將會(huì)開(kāi)始投放使用,因此如果各位在使用過(guò)程中有任何疑問(wèn),都可以通過(guò)企鵝群:150445731進(jìn)行交流,或者獲取Kratos的構(gòu)件。






