進入阿里必備知識-第三步-TDDL資料框架
TDDL(TAOBAO DISTRIBUTE DATA LAYER)
引用
前言
在開始講解淘寶的TDDL(Taobao Distribute Data Layer)技術之前,請允許筆者先吐槽一番。首先要開噴的是淘寶的社群支援做的無比的爛,TaoCode開源社群上面,幾乎從來都是有人提問,無人響應。再者版本迭代速度也同樣差強人意,就目前而言TDDL5.0的版本已經全線開源(Group、Atom、Matrix)大家可以在Github上下載原始碼。
目錄
一、網際網路當下的資料庫拆分過程
二、TDDL的架構原型
三、下載TDDL的Atom層和Group層原始碼
四、Diamond簡介
五、Diamond的安裝和使用
六、動態資料來源層的Master/Salve讀寫分離配置與實現
七、Matrix層的分庫分表配置與實現(此章節由於特殊原因暫時隱藏)
一、網際網路當下的資料庫拆分過程
對於一個剛上線的網際網路專案來說,由於前期活躍使用者數量並不多,併發量也相對較小,所以此時企業一般都會選擇將所有資料存放在一個數據庫中進行訪問操作。但隨著後續的市場推廣力度不斷加強,使用者數量和併發量不斷上升,這時如果僅靠一個數據庫來支撐所有訪問壓力,幾乎是在自尋死路。所以一旦到了這個階段,大部分Mysql DBA就會將資料庫設定成讀寫分離狀態,也就是一個Master節點對應多個Salve節點。經過Master/Salve模式的設計後,完全可以應付單一資料庫無法承受的負載壓力,並將訪問操作分攤至多個Salve節點上,實現真正意義上的讀寫分離。但大家有沒有想過,單一的Master/Salve模式又能抗得了多久呢?如果使用者數量和併發量出現量級
圖1-1 水平分割槽
上述筆者簡單的講解了資料庫的分庫分表原理。接下來請大家認真思考下。原本一個數據庫能夠完成的訪問操作,現在如果按照分庫分表模式設計後,將會顯得非常麻煩,這種麻煩尤其體現在訪問操作上。因為持久層需要判斷出對應的資料來源,以及資料來源上的水平分割槽,這種訪問方式我們稱之為訪問“路由”。按照常理來說,持久層不應該負責資料訪問層(DAL)的工作,它應該只關心one to one的操作形式,所以淘寶的TDDL框架誕生也就順其自然了。
二、TDDL的架構原型
淘寶根據自身業務需求研發了TDDL(Taobao Distributed Data Layer)框架,主要用於解決分庫分表場景下的訪問路由(持久層與資料訪問層的配合)以及異構資料庫之間的資料同步,它是一個基於集中式配置的JDBC DataSource實現,具有分庫分表、Master/Salve、動態資料來源配置等功能。
就目前而言,許多大廠也在出一些更加優秀和社群支援更廣泛的DAL層產品,比如Hibernate Shards、Ibatis-Sharding等。如果你要問筆者還為什麼還要對TDDL進行講解,那麼筆者只能很無奈的表示公司要這麼幹,因為很多時候技術選型並不是筆者說了算,而是客戶說了算。當筆者費勁所有努力在google上尋找TDDL的相關使用說明和介紹時,心裡一股莫名的火已經開始在蔓延,對於更新緩慢(差不多一年沒更新過SVN),幾乎沒社群支援(提問從不響應)的產品來說,除了蝸居在企業內部,必定走不了多遠,最後的結局註定是悲哀的。好了,既然抱怨了一番,無論如何還是要堅持講解完。TDDL位於資料庫和持久層之間,它直接與資料庫建立交道,如圖1-2所示:
圖1-2 TDDL所處領域模型定位
傳說淘寶很早以前就已經對資料進行過分庫分表處理,應用層連線多個數據源,中間有一個叫做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方式傳遞這些資訊;
三、下載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的匯出命令,將原始碼匯出成構件形式也可以。
四、Diamond簡介
使用任何一種框架都需要配置一些配置源資訊,畢竟每一種框架都有自己的規範,使用者務必遵守這些規範來實現自己的業務與基礎框架的整合。自然TDDL也不例外,也是有配置資訊需要顯式的進行配置,在TDDL中,配置可以基於2種方式,一種是基於本地配置檔案的形式,另外一種則是基於Diamond的形式進行配置,在實際開發過程中,由於考慮到配置資訊的集中管理所帶來的好處,大部分開發人員願意選擇將TDDL的配置資訊託管給Diamond,所以本文還是以Diamond作為TDDL的配置源。
diamond是淘寶內部使用的一個管理持久配置的系統,它的特點是簡單、可靠、易用,目前淘寶內部絕大多數系統的配置,由diamond來進行統一管理。diamond為應用系統提供了獲取配置的服務,應用不僅可以在啟動時從diamond獲取相關的配置,而且可以在執行中對配置資料的變化進行感知並獲取變化後的配置資料。
五、Diamond的安裝和使用
Diamond和TDDL不同,它已經實現了完全意義上的開源。大家可以從淘寶的TaoCode
首先我們需要安裝好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的安裝。
六、動態資料來源層的Master/Salve讀寫分離配置與實現
其實使用TDDL並不複雜,只要你會使用JDBC,那麼TDDL對於你來說無非就只需要將JDBC的操作連線替換為TDDL的操作連線,剩餘操作一模一樣。並且由於TDDL遵循了JDBC規範,所以你完全還可以使用Spring JDBC、Hibernate等第三方持久層框架進行ORM操作。
我們來看看如何TDDL中配置TDDL的讀寫分離,Atom+Group組成了TDDL的動態資料來源,這2層主要負責資料庫的讀寫分離。
TGroupDataSource的配置
1、 配置讀寫分離權重:
KEY:com.taobao.tddl.jdbc.group_V2.4.1_“groupKey”(Matrix中為“dbKey”)
VALUE:dbKey:r10w0,dbKey2:r0w10
TAtomDataSource的配置(由3部分組成,global、app、user)
1、 基本資料來源資訊(global):
KEY:com.taobao.tddl.atom.global.“dbKey”
VALUE:(
ip=資料庫IP
port=資料庫埠
dbName=資料庫暱稱
dbType=資料庫型別
dbStatus=RW)
2、 資料庫密碼資訊(user):
KEY:com.taobao.tddl.atom.passwd.“dbName”.“dbType”.“dbUserName”
VALUE:資料庫密碼
3、 資料庫連線資訊(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(); } } } } |
七、Matrix層的分庫分表配置與實現
在上一章節中,筆者演示瞭如何在Diamond中配置資料庫的讀寫分離,那麼本章筆者則會演示如果配置TDDL的分庫分表。
TDDL的Matrix層是建立在動態資料來源之上的,所以分庫分表的配置和讀寫分離的基本配置也是一樣的,只不過我們需要新新增dbgroups和shardrule項。dbgroups項包含了我們所需要配置的所有AppName選項,而shardrule則是具體的分庫分表規則。這裡有一點需要提醒各位,在開源版本的TDDL中,配置TGroupDataSource讀寫分離是使用dbKey,然而在Matrix中則是使用appName。
1、配置Group組:
KEY:com.taobao.tddl.v1_“appName”_dbgroups
VALUE:appName1,appName2
2、配置分庫分表規則:
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(); } } } } |