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();
            }
        }
    }
}
相关文章
相关标签/搜索