前言
在開始講解淘寶的TDDL(Taobao Distribute Data Layer)技術之前,請允許筆者先吐槽一番。首先要開噴的是淘寶的社區支持做的無比的爛,TaoCode開源社區上面,幾乎從來都是有人提問,無人響應。再者版本迭代速度也同樣差強人意,就目前而言TDDL5.0的版本已經全線開源(Group、Atom、Matrix)大家可以在Github上下載源碼。
目錄
一、互聯網當下的數據庫拆分過程
二、TDDL的架構原型
三、下載TDDL的Atom層和Group層源代碼
四、Diamond簡介
五、Diamond的安裝和使用
六、動態數據源層的Master/Salve讀寫分離配置與實現
七、Matrix層的分庫分表配置與實現
一、互聯網當下的數據庫拆分過程
- 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_table1、user_table2。子表之間通過某種契約關聯在一起,每一張子表均按段位進行數據存儲,比如user_table0存儲1-10000的數據,而user_table1存儲10001-20000的數據,最后user_table3存儲20001-30000的數據。經過水平分區設置后的業務表,必然能夠將原本一張表維護的海量數據分配給N個子表進行存儲和維護,這樣的設計在國內一流的互聯網企業比較常見,如圖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所示:
傳說淘寶很早以前就已經對數據進行過分庫分表處理,應用層連接多個數據源,中間有一個叫做DBRoute的技術對數據庫進行統一的路由訪問。DBRoute對數據進行多庫的操作、數據的整合,讓應用層像操作一個數據源一樣操作多個數據庫。但是隨着數據量的增長,對於庫表的分法有了更高的要求,例如,你的商品數據到了百億級別的時候,任何一個庫都無法存放了,於是分成2個、4個、8個、16個、32個……直到1024個、2048個。好,分成這么多,數據能夠存放了,那怎么查詢它?這時候,數據查詢的中間件就要能夠承擔這個重任了,它對上層來說,必須像查詢一個數據庫一樣來查詢數據,還要像查詢一個數據庫一樣快(每條查詢要求在幾毫秒內完成),TDDL就承擔了這樣一個工作(其他DAL產品做得更好),如圖1-3所示:
圖1-3 TDDL分庫分表查詢策略
上述筆者描述了TDDL在分庫分表環境下的查詢策略,那么接下來筆者有必要從淘寶官方copy它們自己對TDDL優點的一些描述,真實性不敢保證,畢竟沒完全開源,和社區零支持,大家看一看就算了,別認真。
TDDL優點:
- 數據庫主備和動態切換;
- 帶權重的讀寫分離;
- 單線程讀重試;
- 集中式數據源信息管理和動態變更;
- 剝離的穩定jboss數據源;
- 支持mysql和oracle數據庫;
- 基於jdbc規范,很容易擴展支持實現jdbc規范的數據源;
- 無server,client-jar形式存在,應用直連數據庫;
- 讀寫次數,並發度流程控制,動態變更;
- 可分析的日志打印,日志流控,動態變更;
注意: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的官方原理解釋:
- TDDL除了拿到分庫分表條件外,還需要拿到order by、group by、limit、join等信息,SUM、MAX、MIN等聚合函數信息,DISTINCT信息。具有這些關鍵字的SQL將會在單庫和多庫情況下進行,語義是不同的。TDDL必須對使用這些關鍵字的SQL返回的結果做出合適的處理;
- TDDL行復制需要重新拼寫SQL,帶上sync_version字段;
- 不通過sql解析,因為TDDL遵守JDBC規范,它不可能去擴充JDBC規范里面的接口,所以只能通過SQL中加額外的字符條件(也就是HINT方式)或者ThreadLocal方式進行傳遞,前者使SQL過長,后者難以維護,開發debug時不容易跟蹤,而且需要判定是在一條SQL執行后失效還是1個連接關閉后才失效;
- 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上下載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的安裝。
六、動態數據源層的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();
}
}
}
}