天天看點

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

分庫分表用于應對目前網際網路常見的兩個場景——大資料量和高并發。通常分為垂直拆分和水準拆分兩種。

垂直拆分是根據業務将一個庫(表)拆分為多個庫(表)。如:将經常和不常通路的字段拆分至不同的庫或表中。由于與業務關系密切,目前的分庫分表産品均使用水準拆分方式。

水準拆分則是根據分片算法将一個庫(表)拆分為多個庫(表)。如:按照ID的最後一位以3取餘,尾數是1的放入第1個庫(表),尾數是2的放入第2個庫(表)等。

關系型資料庫在大于一定資料量的情況下檢索性能會急劇下降。在面對網際網路海量資料情況時,所有資料都存于一張表,顯然會輕易超過資料庫表可承受的資料量閥值。這個單表可承受的資料量閥值,需根據資料庫和并發量的差異,通過實際測試獲得。

單純的分表雖然可以解決資料量過大導緻檢索變慢的問題,但無法解決過多并發請求通路同一個庫,導緻資料庫響應變慢的問題。是以通常水準拆分都至少要采用分庫的方式,用于一并解決大資料量和高并發的問題。這也是部分開源的分片資料庫中間件隻支援分庫的原因。

但分表也有不可替代的适用場景。最常見的分表需求是事務問題。同在一個庫則不需考慮分布式事務,善于使用同庫不同表可有效避免分布式事務帶來的麻煩。目前強一緻性的分布式事務由于性能問題,導緻使用起來并不一定比不分庫分表快。目前采用最終一緻性的柔性事務居多。分表的另一個存在的理由是,過多的資料庫執行個體不利于運維管理。綜上所述,最佳實踐是合理地配合使用分庫+分表。

事務原理連結

Sharding-JDBC是當當應用架構ddframe中,從關系型資料庫子產品dd-rdb中分離出來的資料庫水準分片架構,實作透明化資料庫分庫分表通路。Sharding-JDBC是繼dubbox和elastic-job之後,ddframe系列開源的第3個項目。

Sharding-JDBC直接封裝JDBC API,可以了解為增強版的JDBC驅動,舊代碼遷移成本幾乎為零:

可适用于任何基于Java的ORM架構,如JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC。

可基于任何第三方的資料庫連接配接池,如DBCP、C3P0、 BoneCP、Druid等。

理論上可支援任意實作JDBC規範的資料庫。雖然目前僅支援MySQL,但已有支援Oracle、SQLServer等資料庫的計劃。

Sharding-JDBC定位為輕量Java架構,使用用戶端直連資料庫,以jar包形式提供服務,無proxy代理層,無需額外部署,無其他依賴,DBA也無需改變原有的運維方式。

Sharding-JDBC分片政策靈活,可支援等号、between、in等多元度分片,也可支援多分片鍵。

SQL解析功能完善,支援聚合、分組、排序、limit、or等查詢,并支援Binding Table以及笛卡爾積表查詢。

為了對其他開源項目表示尊重,我們無意評論目前仍在更新中的項目。這裡僅列出目前停止更新,但仍然在資料庫分片領域非常有影響力的幾個項目,請參見表1。

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

表1 資料庫分片工具對比

通過以上表格可以看出,Cobar屬于中間層方案,在應用程式和MySQL之間搭建一層Proxy。中間層介于應用程式與資料庫間,需要做一次轉發,而基于JDBC協定并無額外轉發,直接由應用程式連接配接資料庫,性能上有些許優勢。這裡并非說明中間層一定不如用戶端直連,除了性能,需要考慮的因素還有很多,中間層更便于實作監控、資料遷移、連接配接管理等功能。

Cobar-Client、TDDL和Sharding-JDBC均屬于用戶端直連方案。此方案的優勢在于輕便、相容性、性能以及對DBA影響小。其中Cobar-Client的實作方式基于ORM(Mybatis)架構,其相容性與擴充性不如基于JDBC協定的後兩者。

前文已介紹了Sharding-JDBC是實作了JDBC協定的jar檔案。基于JDBC協定的實作與基于MySQL等資料庫協定實作的中間層略有差别。

無論使用哪種架構,核心邏輯均極為相似,除了協定實作層不同(JDBC或資料庫協定),都會分為分片規則配置、SQL解析、SQL改寫、SQL路由、SQL執行以及結果歸并等子產品。

Sharding-JDBC的整體架構圖參見圖1。

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

圖1 Sharding-JDBC的整體架構圖

Sharding-JDBC的分片邏輯非常靈活,支援分片政策自定義、複數分片鍵、多運算符分片等功能。

如:根據使用者ID分庫,根據訂單ID分表這種分庫分表結合的分片政策;或根據年分庫,月份+使用者區域ID分表這樣的多片鍵分片。

Sharding-JDBC除了支援等号運算符進行分片,還支援in/between運算符分片,提供了更加強大的分片功能。

Sharding-JDBC提供了spring命名空間用于簡化配置,以及規則引擎用于簡化政策編寫。由于目前剛開源分片核心邏輯,這兩個子產品暫未開源,待核心穩定後将會開源其他子產品。

Sharding-JDBC對JDBC規範的重寫思路是針對DataSource、Connection、Statement、PreparedStatement和ResultSet五個核心接口封裝,将多個真實JDBC實作類集合(如:MySQL JDBC實作/DBCP JDBC實作等)納入Sharding-JDBC實作類管理。

Sharding-JDBC盡量最大化實作JDBC協定,包括addBatch這種在JPA中會使用的批量更新功能。但分片JDBC畢竟與原生JDBC不同,是以目前仍有未實作的接口,包括Connection遊标,存儲過程和savePoint相關、ResultSet向前周遊和修改等不太常用的功能。此外,為了保證相容性,并未實作JDBC 4.1及其後釋出的接口(如:DBCP 1.x版本不支援JDBC 4.1)。

SQL解析作為分庫分表類産品的核心,性能和相容性是最重要的衡量名額。目前常見的SQL解析器主要有fdb/jsqlparser和Druid。Sharding-JDBC使用Druid作為SQL解析器,經實際測試,Druid解析速度是另外兩個解析器的幾十倍。

目前Sharding-JDBC支援join、aggregation(包括avg)、order by、 group by、limit、甚至or查詢等複雜SQL的解析。目前不支援union、部分子查詢、函數内分片等不太應在分片場景中出現的SQL解析。

SQL改寫分為兩部分,一部分是将分表的邏輯表名稱替換為真實表名稱。另一部分是根據SQL解析結果替換一些在分片環境中不正确的功能。這裡具兩個例子:

第1個例子是avg計算。在分片的環境中,以avg1 +avg2+avg3/3計算平均值并不正确,需要改寫為(sum1+sum2+sum3)/(count1+count2+ count3)。這就需要将包含avg的SQL改寫為sum和count,然後再結果歸并時重新計算平均值。

第2個例子是分頁。假設每10條資料為一頁,取第2頁資料。在分片環境下擷取limit 10, 10,歸并之後再根據排序條件取出前10條資料是不正确的結果。正确的做法是将分條件改寫為limit 0, 20,取出所有前2頁資料,再結合排序條件算出正确的資料。可以看到越是靠後的Limit分頁效率就會越低,也越浪費記憶體。有很多方法可避免使用limit進行分頁,比如建構記錄行記錄數和行偏移量的二級索引,或使用上次分頁資料結尾ID作為下次查詢條件的分頁方式。

SQL路由是根據分片規則配置,将SQL定位至真正的資料源。主要分為單表路由、Binding表路由和笛卡爾積路由。

單表路由最為簡單,但路由結果不一定落入唯一庫(表),因為支援根據between和in這樣的操作符進行分片,是以最終結果仍然可能落入多個庫(表)。

Binding表可了解為分庫分表規則完全一緻的主從表。舉例說明:訂單表和訂單詳情表都根據訂單ID作為分片鍵,任意時刻分片邏輯均相同。這樣的關聯查詢和單表查詢難度和性能相當。

笛卡爾積查詢最為複雜,因為無法根據Binding關系定位分片規則的一緻性,是以非Binding表的關聯查詢需要拆解為笛卡爾積組合執行。查詢性能較低,而且資料庫連接配接數較高,需謹慎使用。

路由至真實資料源後,Sharding-JDBC将采用多線程并發執行SQL,并完成對addBatch等批量方法的處理。

結果歸并包括4類:普通周遊類、排序類、聚合類和分組類。每種類型都會先根據分頁結果跳過不需要的資料。

普通周遊類最為簡單,隻需按順序周遊ResultSet的集合即可。

排序類結果将結果先排序再輸出,因為各分片結果均按照各自條件完成排序,是以采用歸并排序算法整合最終結果。

聚合類分為3種類型,比較型、累加型和平均值型。比較型包括max和min,隻傳回最大(小)結果。累加型包括sum和count,需要将結果累加後傳回。平均值則是通過SQL改寫的sum和count計算,相關内容已在SQL改寫涵蓋,不再贅述。

分組類最為複雜,需要将所有的ResultSet結果放入記憶體,使用map-reduce算法分組,最後根據排序和聚合條件做相關處理。最消耗記憶體,最損失性能的部分即是此,可以考慮使用limit合理的限制分組資料大小。

結果歸并部分目前并未采用管道解析的方式,之後會針對這裡做更多改進。

路由結果在單庫單表的性能測試報告:

查詢操作:Sharding-JDBC的TPS為JDBC的TPS的99.8%; 

插入操作:Sharding-JDBC的TPS為JDBC的TPS的90.2%; 

更新操作:Sharding-JDBC的TPS為JDBC的TPS的93.1%; 

可以看到,Sharding-JDBC性能損失非常低。

路由結果在多庫多表的性能測試報告:

查詢操作:TPS雙庫比單庫可以增加大約94%的性能; 

插入操作:TPS雙庫比單庫可以增加大約60%的性能; 

更新操作:TPS雙庫比單庫可以增加大約89%的性能; 

結果表明,Sharding-JDBC可有效利用多線程與分布式資源大幅度提升性能; 

更多詳細情況可檢視Sharding-JDBC的性能測試報告。

目前Sharding-JDBC集中于分庫分表核心邏輯開發,在功能穩定之後将會按照如下線路持續更新:

讀寫分離;

柔性分布式事務;

分布式主鍵生成政策;

SQL重寫優化,進一步提升性能;

SQL Hint,可指定某SQL在某具體庫表執行,基于業務規則而非SQL解析路由; 

小表廣播;

HA相關;

流量控制;

資料庫建表工具;

資料遷移;

複雜SQL解析支援,如子查詢、存儲過程等;

Oracle, SQLServer支援;

配置中心;

 最近忙于項目已經好久幾天沒寫部落格了,前2篇文章我給大家介紹了搭建基礎springMvc+mybatis的maven工程,這個簡單架構已經可以對付一般的小型項目。但是我們實際項目中會碰到很多複雜的場景,比如資料量很大的情況下如何保證性能。今天我就給大家介紹資料庫分庫分表的優化,本文介紹mybatis結合當當網的sharding-jdbc分庫分表技術(原理這裡不做介紹)

  首先在pom檔案中引入需要的依賴

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能
解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

  二、建立一個sharding-jdbc.xml檔案,實作分庫分表的配置

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能
解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

  這裡我簡單介紹下一些屬性的含義,

   <rdb:strategy id="tableShardingStrategy" sharding-columns="user_id" algorithm-class="com.meiren.member.common.sharding.MemberSingleKeyTableShardingAlgorithm"/>  配置分表規則器  sharding-columns:分表規 則 

  依賴的名(根據user_id取模分表),algorithm-class:分表規則的實作類 

  <rdb:sharding-rule data-sources="dataSource"> 這裡填寫關聯資料源(多個資料源用逗号隔開),

  <rdb:table-rule logic-table="member_index" actual-tables="member_index_tbl_${[0,1,2,3,4,5,6,7,8,9]}${0..9}"  table-strategy="tableShardingStrategy"/>  logic-table:邏輯表名(mybatis中代替的表名)actual-tables:

  資料庫實際的表名,這裡支援inline表達式,比如:member_index_tbl_${0..2}會解析成member_index_tbl_0,member_index_tbl_1,member_index_tbl_2;member_index_tbl_${[a,b,c]}會被解析成

    member_index_tbl_a,member_index_tbl_b和member_index_tbl_c,兩種表達式一起使用的時候,會采取笛卡爾積的方式:member_index_tbl_${[a,b]}${0..2}解析為member_index_tbl_a0,member_index_tbl_a1                                       member_index_tbl_a2,member_index_tbl_b0,member_index_tbl_b1,member_index_tbl_b2;table-strategy:前面定義的分表規則器;

     三、配置好改檔案後,需要修改之前我們的spring-dataSource的幾個地方,把sqlSessionFactory和transactionManager原來關聯的dataSource統一修改為shardingDataSource(這一步作用就是把資料源全部托管給sharding去管理)

  

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能
解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

 四、實作分表(分庫)邏輯,我們的分表邏輯類需要實作SingleKeyTableShardingAlgorithm接口的三個方法doBetweenSharding、doEqualSharding、doInSharding

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能
解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

五、以上四步,我們就完成了sharding-jdbc的搭建,我們可以寫一個測試demo來檢查我們的成果

<col>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

​<code>​private​</code>​  ​<code>​static​</code>​  ​<code>​final​</code>​  ​<code>​String SERVICE_PROVIDER_XML = ​</code>​ ​<code>​"/spring/member-service.xml"​</code>​ ​<code>​;​</code>​

 ​<code>​private​</code>​  ​<code>​static​</code>​  ​<code>​final​</code>​  ​<code>​String BEAN_NAME = ​</code>​ ​<code>​"idcacheService"​</code>​ ​<code>​;​</code>​

 ​<code>​private​</code>​  ​<code>​ClassPathXmlApplicationContext context = ​</code>​ ​<code>​null​</code>​ ​<code>​;​</code>​

 ​<code>​IdcacheServiceImpl bean = ​</code>​ ​<code>​null​</code>​ ​<code>​;​</code>​

 ​<code>​IdcacheDao idcacheDao;​</code>​

 ​<code>​@Before​</code>​

 ​<code>​public​</code>​  ​<code>​void​</code>​  ​<code>​before() {​</code>​

 ​<code>​context= ​</code>​ ​<code>​new​</code>​  ​<code>​ClassPathXmlApplicationContext(​</code>​

 ​<code>​new​</code>​  ​<code>​String[] {SERVICE_PROVIDER_XML});​</code>​

 ​<code>​idcacheDao=context.getBean(​</code>​ ​<code>​"IdcacheDao"​</code>​ ​<code>​, IdcacheDao.​</code>​ ​<code>​class​</code>​ ​<code>​);​</code>​

 ​<code>​}​</code>​

 ​<code>​@Test​</code>​

 ​<code>​public​</code>​  ​<code>​void​</code>​  ​<code>​getAllCreditActionTest() {​</code>​

 ​<code>​// int id = bean.insertIdcache();​</code>​

 ​<code>​Long s=100l;​</code>​

 ​<code>​MemberDetailsDO memberDetailsDO=idcacheDao.getDetailsById(s);​</code>​

 ​<code>​System.out.println(​</code>​ ​<code>​"QQ---------------------"​</code>​ ​<code>​+memberDetailsDO.getQq());​</code>​

  列印sql語句,輸出結果:QQ-------------------------------------100,證明成功!

解讀分庫分表中間件Sharding-JDBC與實作分庫分表功能

  注意點:這次搭建過程中,我有碰到一個小坑,就是執行的時候會報錯:,官方文檔是有解決方案:引入 &lt;context:property-placeholder location="classpath:/member_service.properties" ignore-unresolvable="true" /&gt;  ,引入這行代碼的時候,·必須要要把這邊管理配配置檔案的bean删除,換句話說,即Spring容器僅允許最多定義一個PropertyPlaceholderConfigurer(或&lt;context:property-placeholder/&gt;),其餘的會被Spring忽略掉(當時搞了半天啊)