TDDL剖析

前言java

在開始講解淘寶的TDDL(Taobao Distribute Data Layer)技術以前,請容許筆者先吐槽一番。首先要開噴的是淘寶的社區支持作的無比的爛,TaoCode開源社區上面,幾乎歷來都是有人提問,無人響應。再者版本迭代速度也一樣差強人意,就目前而言TDDL5.0的版本已經全線開源(Group、Atom、Matrix)你們能夠在Github上下載源碼。mysql

 

目錄web

1、互聯網當下的數據庫拆分過程算法

2、TDDL的架構原型spring

3、下載TDDL的Atom層和Group層源代碼sql

4、Diamond簡介數據庫

5、Diamond的安裝和使用tomcat

6、動態數據源層的Master/Salve讀寫分離配置與實現架構

7、Matrix層的分庫分表配置與實現
併發

 

1、互聯網當下的數據庫拆分過程
  • Phrase 1 單庫 ->讀寫分離

對於一個剛上線的互聯網項目來講,因爲前期活躍用戶數量並很少,併發量也相對較小,因此此時企業通常都會選擇將全部數據存放在一個數據庫中進行訪問操做。但隨着後續的市場推廣力度不斷增強,用戶數量和併發量不斷上升,這時若是僅靠一個數據庫來支撐全部訪問壓力,幾乎是在自尋死路。因此一旦到了這個階段,大部分Mysql DBA就會將數據庫設置成讀寫分離狀態,也就是一個Master節點對應多個Salve節點。

  • Phrase 2 讀寫分離 ->垂直分區(分庫)

通過Master/Salve模式的設計後,徹底能夠應付單一數據庫沒法承受的負載壓力,並將訪問操做分攤至多個Salve節點上,實現真正意義上的讀寫分離。但你們有沒有想過,單一的Master/Salve模式又能抗得了多久呢?若是用戶數量和併發量出現量級上升,單一的Master/Salve模式照樣抗不了多久,畢竟一個Master節點的負載仍是相對比較高的。爲了解決這個難題,Mysql DBA會在單一的Master/Salve模式的基礎之上進行數據庫的垂直分區(分庫)。

所謂垂直分區指的是能夠根據業務自身的不一樣,將本來冗餘在一個數據庫內的業務表拆散,將數據分別存儲在不一樣的數據庫中,同時仍然保持Master/Salve模式。通過垂直分區後的Master/Salve模式徹底能夠承受住不可思議的高併發訪問操做,可是否能夠永遠高枕無憂了?答案是否認的。

  • Phrase 3 垂直分區 ->水平分區(分表)

一旦業務表中的數據量大了,從維護和性能角度來看,不管是任何的CRUD操做,對於數據庫而言都是一件極其耗費資源的事情。即使設置了索引,仍然沒法掩蓋由於數據量過大從而致使的數據庫性能降低的事實,所以這個時候Mysql DBA或許就該對數據庫進行水平分區(分表,sharding)。

所謂水平分區指的是將一個業務表拆分紅多個子表,好比user_table0、user_table一、user_table2。子表之間經過某種契約關聯在一塊兒,每一張子表均按段位進行數據存儲,好比user_table0存儲1-10000的數據,而user_table1存儲10001-20000的數據,最後user_table3存儲20001-30000的數據。通過水平分區設置後的業務表,必然可以將本來一張表維護的海量數據分配給N個子表進行存儲和維護,這樣的設計在國內一流的互聯網企業比較常見,如圖1-1所示:

上述筆者簡單的講解了數據庫的分庫分表原理。接下來請你們認真思考下。本來一個數據庫可以完成的訪問操做,如今若是按照分庫分表模式設計後,將會顯得很是麻煩,這種麻煩尤爲體如今訪問操做上。由於持久層須要判斷出對應的數據源,以及數據源上的水平分區,這種訪問方式咱們稱之爲訪問「路由」。按照常理來講,持久層不該該負責數據訪問層(DAL)的工做,它應該只關心one to one的操做形式,因此淘寶的TDDL框架誕生也就順其天然了。

2、TDDL的架構原型

淘寶根據自身業務需求研發了TDDL(Taobao Distributed Data Layer)框架,主要用於解決分庫分表場景下的訪問路由(持久層與數據訪問層的配合)以及異構數據庫之間的數據同步,它是一個基於集中式配置的JDBC DataSource實現,具備分庫分表、Master/Salve、動態數據源配置等功能。

就目前而言,許多大廠也在出一些更加優秀和社區支持更普遍的DAL層產品,好比Hibernate Shards、Ibatis-Sharding等。若是你要問筆者還爲何還要對TDDL進行講解,那麼筆者只能很無奈的表示公司要這麼幹,由於不少時候技術選型並非筆者說了算,而是客戶說了算。當筆者費勁全部努力在google上尋找TDDL的相關使用說明和介紹時,內心一股莫名的火已經開始在蔓延,對於更新緩慢(差很少一年沒更新過SVN),幾乎沒社區支持(提問從不響應)的產品來講,除了蝸居在企業內部,一定走不了多遠,最後的結局註定是悲哀的。好了,既然抱怨了一番,不管如何仍是要堅持講解完。TDDL位於數據庫和持久層之間,它直接與數據庫創建交道,如圖1-2所示:

 

 

傳說淘寶很早之前就已經對數據進行過度庫分表處理,應用層鏈接多個數據源,中間有一個叫作DBRoute的技術對數據庫進行統一的路由訪問。DBRoute對數據進行多庫的操做、數據的整合,讓應用層像操做一個數據源同樣操做多個數據庫。可是隨着數據量的增加,對於庫表的分法有了更高的要求,例如,你的商品數據到了百億級別的時候,任何一個庫都沒法存放了,因而分紅2個、4個、8個、16個、32個……直到1024個、2048個。好,分紅這麼多,數據可以存放了,那怎麼查詢它?這時候,數據查詢的中間件就要可以承擔這個重任了,它對上層來講,必須像查詢一個數據庫同樣來查詢數據,還要像查詢一個數據庫同樣快(每條查詢要求在幾毫秒內完成),TDDL就承擔了這樣一個工做(其餘DAL產品作得更好),如圖1-3所示:

 

圖1-3 TDDL分庫分表查詢策略

上述筆者描述了TDDL在分庫分表環境下的查詢策略,那麼接下來筆者有必要從淘寶官方copy它們本身對TDDL優勢的一些描述,真實性不敢保證,畢竟沒徹底開源,和社區零支持,你們看一看就算了,別認真。

TDDL優勢:

  1. 數據庫主備和動態切換;
  2. 帶權重的讀寫分離;
  3. 單線程讀重試;
  4. 集中式數據源信息管理和動態變動;
  5. 剝離的穩定jboss數據源;
  6. 支持mysql和oracle數據庫;
  7. 基於jdbc規範,很容易擴展支持實現jdbc規範的數據源;
  8. 無server,client-jar形式存在,應用直連數據庫;
  9. 讀寫次數,併發度流程控制,動態變動;
  10. 可分析的日誌打印,日誌流控,動態變動;

 

注意:TDDL必需要依賴diamond配置中心(diamond是淘寶內部使用的一個管理持久配置的系統,目前淘寶內部絕大多數系統的配置)。

 接下來,筆者將會帶領各位一塊兒分析TDDL的體系架構。TDDL其實主要能夠劃分爲3層架構,分別是Matrix層、Group層和Atom層。Matrix層用於實現分庫分表邏輯,底層持有多個Group實例。而Group層和Atom共同組成了動態數據源,Group層實現了數據庫的Master/Salve模式的寫分離邏輯,底層持有多個Atom實例。最後Atom層(TAtomDataSource)實現數據庫ip,port,password,connectionProperties等信息的動態推送,以及持有原子的數據源分離的JBOSS數據源)。

圖1-4 TDDL體系結構

章節的最後,咱們還須要對TDDL的原理進行一次剖析。由於咱們知道持久層只關心對數據源的CRUD操做,而多數據源的訪問,並不該該由它來關心。也就是說TDDL透明給持久層的數據源接口應該是統一且「單一」的,至於數據庫到底如何分庫分表,持久層無需知道,也無需編寫對應的SQL去實行應對策略。這個時候對TDDL一些疑問就出現了,TDDL須要對SQL進行二次解析和拼裝嗎?答案是不解析僅拼裝。說白了TDDL只須要從持久層拿到發出的SQL

再按照一些分庫分表條件,進行特定的SQL擴充以此知足訪問路路由操做。

 

如下是淘寶團隊對TDDL的官方原理解釋:

  1. TDDL除了拿到分庫分表條件外,還須要拿到order by、group by、limit、join等信息,SUM、MAX、MIN等聚合函數信息,DISTINCT信息。具備這些關鍵字的SQL將會在單庫和多庫狀況下進行,語義是不一樣的。TDDL必須對使用這些關鍵字的SQL返回的結果作出合適的處理;
  2. TDDL行復制須要從新拼寫SQL,帶上sync_version字段;
  3. 不經過sql解析,由於TDDL遵照JDBC規範,它不可能去擴充JDBC規範裏面的接口,因此只能經過SQL中加額外的字符條件(也就是HINT方式)或者ThreadLocal方式進行傳遞,前者使SQL過長,後者難以維護,開發debug時不容易跟蹤,並且須要斷定是在一條SQL執行後失效仍是1個鏈接關閉後才失效;
  4. TDDL如今也同時支持Hint方式和ThreadLocal方式傳遞這些信息;

 

3、下載TDDL的Atom層和Group層源代碼

         前面咱們談及了TDDL的動態數據源主要由2部分構成,分別是Atom和Group。Group用於實現數據庫的Master/Salve模式的寫分離邏輯,而Atom層則是持有數據源。很是遺憾的TDDL中還有一層叫作Matrix,該層是整個TDDL最爲核心的地方,淘寶也並無對這一層實現開源,而Matrix層主要是創建在動態數據源之上的分庫分表實現。換句話說,TDDL是基於模塊化結構的,開發人員能夠選用TDDL中的部分子集。

       你們能夠從淘寶的TaoCode上下載TDDL的源碼帶,而後進行構件的打包。TDDL的項目主要是基於Maven進行管理的,因此建議你們若是不瞭解Maven的使用,仍是參考下筆者的博文《Use Maven3.x》

       你們下載好TDDL的源代碼後,經過IDE工具導入進來後能夠發現,開源的TDDL的工程結構有以下幾部份組成:

tddl-all –
       —tbdatasource
       —tddl-atom-datasource
       —tddl-common
       —tddl-group-datasource
       —tddl-interact
       —tddl-sample 

你們可使用Maven的命令「mvn package「將TDDL的源代碼打包成構件。若是你的電腦上並無安裝Maven的插件到不是沒有辦法實現構件打包,你可使用eclipse的導出命令,將源代碼導出成構件形式也能夠。

4、Diamond簡介

       使用任何一種框架都須要配置一些配置源信息,畢竟每一種框架都有本身的規範,使用者務必遵照這些規範來實現本身的業務與基礎框架的整合。天然TDDL也不例外,也是有配置信息須要顯式的進行配置,在TDDL中,配置能夠基於2種方式,一種是基於本地配置文件的形式,另一種則是基於Diamond的形式進行配置,在實際開發過程當中,因爲考慮到配置信息的集中管理所帶來的好處,大部分開發人員願意選擇將TDDL的配置信息託管給Diamond,因此本文仍是以Diamond做爲TDDL的配置源。

       diamond是淘寶內部使用的一個管理持久配置的系統,它的特色是簡單、可靠、易用,目前淘寶內部絕大多數系統的配置,由diamond來進行統一管理。diamond爲應用系統提供了獲取配置的服務,應用不只能夠在啓動時從diamond獲取相關的配置,並且能夠在運行中對配置數據的變化進行感知並獲取變化後的配置數據。

5、Diamond的安裝和使用

       Diamond和TDDL不一樣,它已經實現了徹底意義上的開源。你們能夠從淘寶的TaoCode上下載Diamond的源代碼,SVN下載地址爲http://code.taobao.org/svn/diamond/trunk。當你們成功下載好Diamond的源代碼後,咱們接下來就須要開始Diamond的環境搭建工做。

       首先咱們須要安裝好Mysql數據庫,以root用戶登陸,創建用戶並賦予權限,創建數據庫,而後建表,語句分別以下:

create database diamond;
grant all on diamond.* to zh@’%’  identified by ‘abc’;
use diamond; create table config_info ( ‘id’ bigint(64) unsigned NOT NULL auto_increment, ‘data_id’ varchar(255) NOT NULL default ’ ’, ‘group_id’ varchar(128) NOT NULL default ’ ’, ‘content’ longtext NOT NULL, ‘md5′ varchar(32) NOT NULL default ’’, ‘gmt_create’ datetime NOT NULL default ’2010-05-05 00:00:00′, ‘gmt_modified’ datetime NOT NULL default ’2010-05-05 00:00:00′, PRIMARY KEY (‘id’), UNIQUE KEY ‘uk_config_datagroup’ (‘data_id’,'group_id’));

完成後,請將數據庫的配置信息(IP,用戶名,密碼)添加到diamond-server工程的src/resources/jdbc.properties文件中的db.url,db.user,db.password屬性上面,這裏創建的庫名,用戶名和密碼,必須和jdbc.properties中對應的屬性相同。

tomcat是Damond的運行容器,在diamond-server源代碼根目錄下,執行mvn clean package -Dmaven.test.skip,成功後會在diamond-server/target目錄下生成diamond-server.war。打包完成後,將diamond-server.war放在tomcat的webapps目錄下。最後啓動tomcat,即啓動了Diamond。

http server用來存放diamond server等地址列表,能夠選用任何http server,這裏以tomcat爲例。通常來說,http server和diamond server是部署在不一樣機器上的,這裏簡單起見,將兩者部署在同一個機器下的同一個tomcat的同一個應用中,注意,若是部署在不一樣的tomcat中,端口號必定是8080,不能修改(因此必須部署在不一樣的機器上)。

在tomcat的webapps中的diamond-server中創建文件diamond,文件內容是diamond-server的地址列表,一行一個地址,地址爲IP,例如127.0.0.1,完成這些步驟後,就等於已經完成Diamond的安裝。

 

6、動態數據源層的Master/Salve讀寫分離配置與實現

其實使用TDDL並不複雜,只要你會使用JDBC,那麼TDDL對於你來講無非就只須要將JDBC的操做鏈接替換爲TDDL的操做鏈接,剩餘操做如出一轍。而且因爲TDDL遵循了JDBC規範,因此你徹底還可使用Spring JDBC、Hibernate等第三方持久層框架進行ORM操做。

咱們來看看如何TDDL中配置TDDL的讀寫分離,Atom+Group組成了TDDL的動態數據源,這2層主要負責數據庫的讀寫分離。

TGroupDataSource的配置

一、  配置讀寫分離權重:

KEY:com.taobao.tddl.jdbc.group_V2.4.1_「groupKey」(Matrix中爲「dbKey」)
VALUE:dbKey:r10w0,dbKey2:r0w10

TAtomDataSource的配置(由3部分組成,global、app、user)

一、  基本數據源信息(global):

KEY:com.taobao.tddl.atom.global.「dbKey」
VALUE:(
ip=數據庫IP
port=數據庫端口
dbName=數據庫暱稱
dbType=數據庫類型
dbStatus=RW)

二、  數據庫密碼信息(user):

KEY:com.taobao.tddl.atom.passwd.「dbName」.「dbType」.「dbUserName」
VALUE:數據庫密碼

三、  數據庫鏈接信息(app,若是不配置時間單位,缺省爲分鐘):

KEY:com.taobao.tddl.atom.app.「appName」.「dbKey」
VALUE:(
userName=數據庫用戶
minPoolSize=最小鏈接數
maxPoolSize=最大鏈接數
idleTimeout=鏈接的最大空閒時間
blockingTimeout=等待鏈接的最大時間
checkValidConnectionSQL=select 1
connectionProperties=rewriteBatchedStatements=true&characterEncoding=UTF8&connectTimeout=1000&autoReconnect=true&socketTimeout=12000) 

應用層使用TDDL示例:

public class UseTDDL {
    private static final String APPNAME = "tddl_test";
    private static final String GROUP_KEY = "tddltest";
    private static TGroupDataSource tGroupDataSource;

    /* 初始化動態數據源 */
    static {
        tGroupDataSource = new TGroupDataSource();
        tGroupDataSource.setAppName(APPNAME);
        tGroupDataSource.setDbGroupKey(GROUP_KEY);
        tGroupDataSource.init();
    }

    @Test
    public void testQuery() {
        final String LOAD_USER = "SELECT userName FROM tddl_table WHERE userName=?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = tGroupDataSource.getConnection();
            pstmt = conn.prepareStatement(LOAD_USER);
            pstmt.setString(1, "tddl-test2");
            rs = pstmt.executeQuery();
            while (rs.next())
                System.out.println("data: " + rs.getString(1));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != rs)
                    rs.close();

                if (null != pstmt)
                    pstmt.close();

                if (null != conn)
                    conn.close();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

7、Matrix層的分庫分表配置與實現

在上一章節中,筆者演示瞭如何在Diamond中配置數據庫的讀寫分離,那麼本章筆者則會演示若是配置TDDL的分庫分表。

TDDL的Matrix層是創建在動態數據源之上的,因此分庫分表的配置和讀寫分離的基本配置也是同樣的,只不過咱們須要新添加dbgroups和shardrule項。dbgroups項包含了咱們所須要配置的全部AppName選項,而shardrule則是具體的分庫分表規則。這裏有一點須要提醒各位,在開源版本的TDDL中,配置TGroupDataSource讀寫分離是使用dbKey,然而在Matrix中則是使用appName。

一、配置Group組:

KEY:com.taobao.tddl.v1_「appName」_dbgroups
VALUE:appName1,appName2

二、配置分庫分表規則:   

         KEY:com.taobao.tddl.v1_」appName」_shardrule
         VALUE:(
         <?xml version="1.0" encoding="gb2312"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
         <bean id="root" class="com.taobao.tddl.common.config.beans.AppRule" init-method="init">
                   <property name="readwriteRule" ref="readwriteRule" />
         </bean>

         <bean id="readwriteRule" class="com.taobao.tddl.common.config.beans.ShardRule">
                   <property name="dbtype" value="MYSQL" />
                   <property name="tableRules">
                            <map>
                                     <entry key="tddl_table" value-ref="tddl_table" />
                            </map>
                   </property>
         </bean>

         <bean id="tddl_table"  init-method="init"
                   class="com.taobao.tddl.common.config.beans.TableRule">
                   <!-- 數據庫組index號  -->
         <property name="dbIndexes" value="tddl_test,tddl_test2" />
                   <!--分庫規則-->
                   <property name="dbRuleArray" value="(#id#.longValue() % 4).intdiv(2)"/>
                   <!--分表規則,須要注意的是,由於taobao目前dba的要求是全部庫內的表名必須徹底不一樣,所以這裏多加了一個映射的關係簡單來講,分表規則只會算表的key.
倆庫4表: db1(tab1+tab2) db2(tab3+tab4) db1 == key: 0 value tab1 key: 1 value tab2 db2 == key: 0 value tab3 key: 1 value tab4 --> <property name="tbRuleArray" value="#id#.longValue() % 4 % 2"/> <property name="tbSuffix" value="throughAllDB:[_0-_3]" /> </bean> </beans> )

         TDDL的分庫分表配置形式徹底是採用Spring的配置形式,這一點你們應該是很是熟悉的。那麼接下來咱們一步一步的分析TDDL的分庫分表規則。

         在元素<map/>中咱們能夠定義咱們所須要的分表,也就是說,當有多個表須要實現分表邏輯的時候,咱們能夠在集合中進行定義。固然咱們還須要外部引用<bean/>標籤中定義的具體的表邏輯的分庫分表規則。

         在分庫分表規則中,咱們須要定義數據庫組index號,也就是說咱們須要定義咱們有多少的appNames,接下來咱們就能夠定義分庫和分表規則了。TDDL的分庫分表規則徹底是採用取餘方式,好比<property name="dbRuleArray" value="(#id#.longValue() % 4).intdiv(2)"/>,value屬性中包含有具體的分庫規則,其中「#id#」做爲咱們的分庫分表條件,此值在數據庫中對應的類型必須是整類,而後進行取餘後再進行intdiv。或許有些朋友看不太明白這個是什麼意思,咱們用簡單的一點的話來講就是,「#id#.longValue() % 4).intdiv(2)」的含義是咱們須要分2個庫和4個表,那麼咱們怎麼知道咱們的數據到底落盤到哪個庫呢?打個比方,若是咱們的id等於10,首先10%4等於2,而後2/2等於1,TDDL分庫規則下標從0開始,那麼咱們的數據就是落盤到第2個庫。

         當你們明白TDDL的分庫規則後,咱們接下來再來分析分表規則<property name="tbRuleArray" value="#id#.longValue() % 4 % 2"/>。和分庫規則相似的是,咱們都採用取餘算法首先進行運算,只不過度表尾運算也是使用取餘,而不是除算。打個比方,若是咱們的id等於10,首先10%4等於2,而後2%2等於0,那麼咱們的數據就是落盤到第2個庫的第1張表。

 

應用層使用TDDL示例:

public class UseTDDL {
    private static final String APPNAME = "tddl_test";
    private static final TDataSource dataSource;

    /* 初始化動態數據源 */
    static {
        dataSource = new TDataSource();
        dataSource.setAppName(APPNAME);
        dataSource.setUseLocalConfig(false);
        dataSource.setDynamicRule(false);
        dataSource.init();
    }

    @Test
    public void query() {
        final String LOAD_USER = "SELECT userName FROM tddl_table WHERE id = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(LOAD_USER);
            pstmt.setLong(1, 3);
            rs = pstmt.executeQuery();
            while (rs.next())
                System.out.println("data: " + rs.getString(1));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != rs)
                    rs.close();

                if (null != pstmt)
                    pstmt.close();

                if (null != conn)
                    conn.close();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void insert() {
        final String LOAD_USER = "insert into tddl_table values(?, ?)";
        Connection conn = null;
        PreparedStatement pstmt = null;
        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(LOAD_USER);
            pstmt.setLong(1, 10);
            pstmt.setString(2, "JohnGao");
            pstmt.execute();
            System.out.println("insert success...");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != pstmt)
                    pstmt.close();

                if (null != conn)
                    conn.close();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
相關文章
相關標籤/搜索