剖析淘宝TDDL(TAOBAO DISTRIBUTE DATA LAYER)

博客分类:

      原博客地址: http://qq85609655.iteye.com/blog/2035176
 

剖析淘宝 TDDL ( TAOBAO DISTRIBUTE DATA LAYER 

注:本文部分内容引用 本人 博客 http://gao-xianglong.iteye.com/blog/1973591

 

前言

在开始讲解淘宝的 TDDL(Taobao Distribute Data Layer) 技术之前,请允许笔者先吐槽一番。首先要开喷的是淘宝的社区支持做的无比的烂, TaoCode 开源社区上面,几乎从来都是有人提问,无人响应。再者版本迭代速度也同样差强人意 , 就目前而言 TDDL 的版本已经全线开源(Group、Atom、Matrix)大家可以在Github上下载源码 。

目录

一、互联网当下的数据库拆分过程

二、 TDDL 的架构原型

三、下载 TDDL 的 Atom 层和 Group 层源代码

四、 Diamond 简介

五、 Diamond 的安装和使用

六、动态数据源层的 Master/Salve 读写分离 配置与实现

七、 Matrix 层的分库分表配置与实现

一、互联网当下的数据库拆分过程

对于一个刚上线的互联网项目来说,由于前期活跃用户数量并不多,并发量也相对较小,所以此时企业一般都会选择将所有数据存放在 一个数据库 中进行访问操作。但随着后续的市场推广力度不断加强,用户数量和并发量不断上升,这时如果仅靠一个数据库来支撑所有访问压力,几乎是在 自寻死路 。所以一旦到了这个阶段,大部分 Mysql DBA 就会将数据库设置成 读写分离状态 ,也就是一个 Master节点对应多个 Salve 节点。经过 Master/Salve 模式的设计后,完全可以应付单一数据库无法承受的负载压力,并将访问操作分摊至多个 Salve 节点上,实现真正意义上的读写分离。但大家有没有想过,单一的 Master/Salve 模式又能抗得了多久呢?如果用户数量和并发量出现 量级 上升,单一的 Master/Salve 模式照样抗不了多久,毕竟一个 Master 节点的负载还是相对比较高的。为了解决这个难题,Mysql DBA 会在单一的 Master/Salve 模式的基础之上进行数据库的 垂直分区 (分库)。所谓垂直分区指的是可以根据业务自身的不同,将原本冗余在一个数据库内的业务表拆散,将数据分别存储在不同的数据库中,同时仍然保持 Master/Salve模式。经过垂直分区后的 Master/Salve 模式完全可以承受住难以想象的高并发访问操作,但是否可以永远 高枕无忧 了?答案是否定的,一旦业务表中的数据量大了,从维护和性能角度来看,无论是任何的 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 所示:

[转帖]剖析淘宝TDDL(TAOBAO DISTRIBUTE DATA LAYER)-LMLPHP

图 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 所示:

[转帖]剖析淘宝TDDL(TAOBAO DISTRIBUTE DATA LAYER)-LMLPHP

图 1-2 TDDL 所处领域模型定位

传说淘宝很早以前就已经对数据进行过分库分表处理,应用层连接多个数据源,中间有一个叫做 DBRoute 的技术对数据库进行 统一 的路由访问。 DBRoute 对数据进行多库的操作、数据的整合,让应用层像操作一个数据源一样操作多个数据库。但是随着数据量的增长,对于库表的分法有了更高的要求,例如,你的商品数据到了百亿级别的时候,任何一个库都无法存放了,于是分成 2 个、 4 个、 8 个、 16个、 32 个 …… 直到 1024 个、 2048 个。好,分成这么多,数据能够存放了,那怎么查询它?这时候,数据查询的中间件就要能够承担这个重任了,它对上层来说,必须像查询一个数据库一样来查询数据,还要像查询一个数据库一样快( 每条查询要求在几毫秒内完成 ), TDDL 就承担了这样一个工作( 其他 DAL 产品做得更好 ),如图 1-3 所示:

[转帖]剖析淘宝TDDL(TAOBAO DISTRIBUTE DATA LAYER)-LMLPHP

图 1-3 TDDL 分库分表查询策略

上述笔者描述了 TDDL 在分库分表环境下的查询策略,那么接下来笔者有必要从淘宝官方 copy 它们自己对 TDDL 优点的一些描述,真实性不敢保证,毕竟没完全开源,和社区零支持,大家看一看就算了,别认真。

淘宝人自定的 TDDL 优点:

、数据库主备和动态切换; 
、带权重的读写分离; 
、单线程读重试; 
、集中式数据源信息管理和动态变更; 
、剥离的稳定 jboss 数据源; 
、支持 mysql 和 oracle 数据库; 
、基于 jdbc 规范,很容易扩展支持实现 jdbc 规范的数据源; 
、无 server,client-jar 形式存在,应用直连数据库; 
、读写次数 并发度流程控制,动态变更; 
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 数据源)。

[转帖]剖析淘宝TDDL(TAOBAO DISTRIBUTE DATA LAYER)-LMLPHP

图 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

上下载 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 的配置(由 部分组成, 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. “ dbKey ” . “ 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 

、配置 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();

}

}

}

}

05-01 02:41