Skip to content
agapple edited this page Jan 26, 2014 · 8 revisions

背景

  规则属于TDDL5核心组件之一,其主要用于解决数据的路由问题,将用户SQL定义中的对于一张逻辑表的操作,经过规则计算,可返回需要操作的一张或者多张物理表. 

工作原理

整个流程主要分为三步:

  1. 构造语法树 【主要为Tddl_Optimizer生成的语法树,经过特定的转化]】
  2. 基于语法树查找匹配的规则
  3. 基于规则计算出分库分表的结果

构建语法树

元数据 

说明:

  • 元数据结构比较简单,主要是针对语法树中where条件的处理,每个条件都可以抽象为一组(And/Or)的Comparative组合. 
  • 做为通用组件,抽象了Comparative语法树构造器,允许tddl optimizer基于ASTNode实现,也允许外部用户基于自己特定的结构进行生成.  

默认支持的构造器:

  1. 基于TDDL5 Optimizer的ASTNode,可查看:OptimizerRule相关使用Tddl-Rule组件的代码
  2. 基于string文本解析

基于string文本解析

简单语法: KEY CMP VALUE [:TYPE]

  • KEY: 类似字段名字,用户随意定义
  • CMP: 链接符,比如< = > 等,具体可查看Comparative中的定义
  • VALUE:对应的值,比如1
  • TYPE: 描述VALUE的类型,可选型,如果不填默认为Long类型。支持: int/long/string/date,可以使用首字母做为缩写,比如i/l/s/d

几个例子:

  1. id = 1
  2. id = 1 : long
  3. id > 1 and id < 1 : long
  4. gmt_create = 2011-11-11 : date
  5. id in (1,2,3,4) : long

规则库

说明:

  • TddlRuleConfig做为tddl-rule组件的整个上下文,允许同时存在多个版本.  (比如配合数据做迁移时,动态切换规则的那一段状态)
  • 每个Tddl规则库,都是一个VirtablTableRoot,其管理着多个VirtualTableRule. 
  • 每个VirtualTableRule对应一张逻辑表的规则定义,库规则和表规则,都可以进行定义,定义通过Rule对象来描述. 
  • 每个Rule对象,包含多个RuleColumn,也就是一个分库分表规则可以支持多个字段. 
  • 默认的Rule是先是基于Groovy动态脚本定义,以及支持按照多种类型的分库实现。默认支持按照id做hash,或者按照时间做滚动(多种时间纬度,比如按小时,天,月,年等)

规则定义

规则的定义是整个tddl-rule中最难理解的一部分,所以单独展开来介绍,帮助大家更快理解和上手.  

在介绍之前,首先需要思考几个问题:

  1. 规则中配置都是基于xml,字符串解析后,如何来描述数据的类型,比如这个字段是date类型,需要按照时间对象进行处理. 
  2. sql查询中会有全库扫描的需求,如何通过规则来进行描述整个数据库的分库分表的拓扑结构.  

tddl中会解决这两个问题,特殊设计了一种表达式:#user_id,1_number,4# 

解释一下:

a.  整个表达式通过前后两个#来区分不同的字符串表达式,表达式解析中,会通过#(.*)#进行正则匹配提取. 

b.  表达式中的一个分为3个部分:

  1. user_id表示分表字段名
  2. 1_number标示当前的数据自增步长和类型(下划线_做为分隔符),number对应于AtomIncreaseType中的类型.  (支持number,hour,date,month,year)这几类.  如果是1_hour,那代表是按1小时进行自增. 
  3. 最后一个数字4,可以理解代表整个分表的总数,比如需要做全库扫描时,需要进行遍历的表数.   【针对按照字符串hash时,这个数字会大于分表的总数,这个后面会介绍下】

举几个例子说明第2,3个参数的实际意义:

例子1:

数据表:admin_{00}表,按user_id取模分为4张表.  对应的Rule定义为:#user_id,1_number,4# % 4

SQL :  select * from admin where user_id > 2,对于这样一条sql,tddl为了要知道的是它需要在哪些表上执行

  1. 通过解析sql之后的到的参数名是'user_id',并且comparative参数值是'> 2'
  2. 因为>2是一个范围查询,TDDL开始进行参数枚举: 【#user_id,1,4# % 4 中第二个字段表示枚举的步长,这里配置的值是1, 起始给定的参数值是2】
    (1). user_id = (2+1=3)%4 运算得到 admin_03,枚举次数1
    (2). user_id = (3+1=4)%4 运算得到 admin_00,枚举次数2
    (3). user_id = (4+1=5)%4 运算得到 admin_01,枚举次数3
    (4). user_id = (5+1=6)%4 运算得到 admin_02,枚举次数4
    到此,TDDL发现枚举次数达到了4次,也就是 #user_id,1,4#中第三个参数的值,那么停止枚举. 
  3. 经过枚举,最终得到的表为:admin_03,admin_00,admin_01,admin_01,admin_02,也就是执行了一次全表扫描 【对比sql:select * from admin where user_id > 2,我们的意图也就是进行一次全表扫描】

例子2:

数据表:log_{00}表,按照gmt_create日期做滚动,并且只保留一个月的数据,对应的Rule定义为:#gmt_create,1_date,31# % 32

SQL :  select * from log where gmt_create > '2013-12-24' and gmt_create <= '2013-12-27',对于这样的一条sql,tddl为了要知道的是它需要在哪些表上执行

  1. 通过解析sql之后的到的参数名是'gmt_create',并且comparative参数值为'>2013-12-24 and gmt_create <= 2013-12-31' 
  2. 因为'>2013-12-24 and gmt_create < 2013-12-26' 是个范围查询,TDDL开始进行参数枚举:【#gmt_create,1_date,31# % 31 中第二个字段表示枚举的步长,配置的值是1_date,表式按天滚动】
    (1). gmt_create = (2013-12-24  + 1 day) % 32 运算得到 log_25 【>2013-12-24,是开区间】
    (2). gmt_create = (2013-12-25  + 1 day) % 32 运算得到 log_26
    (3). gmt_create = (2013-12-26  + 1 day) % 32 运算得到 log_27  【<=2013-12-27,是闭区间】
    到此,TDDL发现枚举次数达到了3次,而不是默认定义中的31次,在枚举遍历的时候会和当前的comparative中的范围值进行判断. 
  3. 经过枚举,最终得到的表为:log_25,log_26,log_27,也就是查询这三个日期内对应表的数据.  

规则管理

规则目前是通过spring bean xml来定义Rule,选择spring主要的考虑是减少大家熟悉成本,没有特别的因素.

目前spring只管理Rule对象的创建和销毁,规则计算过程中,不会涉及和spring的相关调用. 

规则的配置方式:

  • 本地规则
  • 远程规则 【建议使用,支持diamond运行时推送,客户端可即时生效,可解决数据扩容时对应用的无缝切换,远程更新规则,客户端不需要发布和重启】

规则计算

工作原理

规则计算主要是针对VirtualTableRule对象,获取其定义的DbRules和TbRules,根据当前Comparative语法树中的参数进行组合计算. 

通过规则定义的章节,已经向大家展示了基本的规则的用法,但实际规则计算中涉及的会相对复杂,复杂度增加的几个因素:

  1. 分库/分表同时存在
  2. 多个分库字段
  3. 可选分库分表字段【比如一个表是按照两个字段进行分库分表,如果只提供一个字段时,需要进行部分表的扫描】
  4. in分库分表枚举值追踪 【需要记录产生这个分库分表结果的枚举值,比如可通过user_id = 1,计算出表在admin_01上,那id in (1,2),会根据计算出表的不同,发到admin_01上的条件就会变为id in (1),admin_02上的条件就会变为id in (2),这样的优化在对于id in的条件数比较多时非常有效】 
  5. 自定义grovvy规则

在介绍规则计算之前,首先要了解规则计算中一个最重要的概念:Samples(枚举值或称描点).   

Samples可以理解为一个排列组合枚举中的一种组合的记录,遍历完整个排列组合后,可以得到一组Samples. 

比如:id in (1,2) and name in ('ljh','hlh'),  id = 1 and name = 'ljh'  就是一个排列组合的值,就是一个Samples. 

了解了基本概念,再来看一下规则计算的流程:

  1. 首先根据ComparativeMapChoicer和当前VirtualTableRule中的Rule定义,找到匹配的分库和分表的Rule.  【一个表允许同时存在多份Rule,根据对应的参数进行自动匹配Rule,运行时选择】
  2. 生成分库分表字段的枚举值列表.  【比如分库字段A的枚举值,分库字段B的枚举值,最终的A,B一起的枚举值就是A * B组合的一个笛卡尔积】
  3. 遍历枚举值列表,调用Rule.calculate()进行计算.  【目前Rule计算的实现使用Groovy】
    比如分表Rule定义: #id,1_number,1024# % 4 ,会被编译编译为Groovy的脚本,对应的#id,1_number,1024#会被替换为map.get("id")的调用. 而对应的map就是枚举值对象。所以每个枚举值对象都会计算出一个结果,遍历完所有的枚举值后进行去重,所得的结果就是分库分表的结果. 

目前支持规则计算的操作符:

  1. GreaterThan(>)
  2. GreaterThanOrEqual(>=)
  3. Equivalent(=)
  4. NotEquivalent(!=)
  5. LessThan(<)
  6. LessThanOrEqual(<=)

sql例子:

  1. where id = 1  【直接hash计算】
  2. where id > 1  【半开区间,会是个全库扫描】
  3. where id >= 1 【半开区间,会是个全库扫描】
  4. where id < 5 【半开区间,会是个全库扫描】
  5. where id <=5  【半开区间,会是个全库扫描】
  6. where id > 1 and id < 4 【部分区间枚举】
  7. where id >=1 and id <=4 【部分区间枚举,between 1 and 4可转化为此模式】
  8. where id = 1 or id = 2 or id =3 【批量hash计算,id in (1,2,3)可转化为此模式】
  9. where id > 1 and id < 10  and ( id = 2 or id =11)  【目前不支持,限制:同一个字段上and操作只允许为两个简单对象,不能出现嵌套and/or,需要进行条件merge处理】

下面具体介绍一下,不同条件下枚举值生成的实现. 

分库/分表同时存在

例子:

  • 分库 (#user_id,1,64#.longValue() % 64).intdiv(32)
  • 分表(#user_id,1,64#.longValue() % 64

比如user表希望进行即分库又分表,比如按照user_id取模划分64张表,表明具体为'user_0000'-'user_0063'

  • 'user_0000'-'user_0031' 在'TDDL_0000_GROUP'中
  • 'user_0032'-'user_0063' 在'TDDL_0001_GROUP'中

此时的枚举逻辑会有所变化,枚举算法:

  1. 首先按照分库的Rule规则进行计算,同时记录每个分库的计算结果对应的枚举值 【比如计算出TDDL_0000_GROUP,对应的枚举值为0~31.】
  2. 然后基于每个分库的计算结果对应的枚举值,再按照分表的Rule规则进行枚举计算,最后得出整个的分库分表规则

总结需要额外考虑问题:

  1. 分库分表有无公共字段 【无影响,各自算各自的】
  2. 分库分表有公共字段 【有影响,先算库规则,基于库规则的枚举值,再算表规则】
    a. 库规则和表规则的步长,类型,范围值都一样
    b. 库规则和表规则的步长,类型,范围值存在不一样【比如库规则是1_month,按照月进行分库,表规则是1_date,按照天进行分表】

多个分库分表字段

相关需求:

  1. 日志按照天进行划分,数据量还过大,需要再按照id进行hash分表.   对应的规则可能就是 #gmt_create,1_date,31# % 32 + "_" + #id,1_number,4# % 4. 

此时的枚举逻辑会有所变化,枚举算法:

  1. 各自计算出单个字段的枚举值列表
  2. 将多个字段的枚举值列表进行全排列算法,得出每一种组合的可能性. 核心代码类Samples.java
  3. 按照分库/分表枚举算法,得出枚举值列表.

可选分库分表字段

相关需求:

  • 全库扫描 【不提供分库字段的条件】
  • 多字段部分表扫描 【比如日志按照时间+id hash进行分库分表,如果用户需要查询每条id的记录时,需要对时间范围进行枚举遍历】

可选配置:(字段信息后加个?代表optional)

#column?,1_number,1024
 

此时的枚举逻辑会有所变化,枚举算法:

  • 计算出存在字段条件的枚举值列表
  • 针对不存在字段条件的,按照Rule定义中的值进行枚举. 定义:#gmt_create,1_date,31#
  • 按照多个分库字段的枚举算法,得出最终的枚举值列表. 

in分库分表枚举值追踪

相关需求:

  • in的批量查询
  • tddl5中join模式的Index Loop Join

目前tddl中对于in的支持是转化为OR模式,比如id in (1,2,3) 会转化为 id = 1 or id = 2 or id = 3. 

此时的枚举逻辑会有所变化,枚举算法:

  1. 计算出条件的枚举值列表
  2. 调用rule进行运算得出分库分表值,同时记录当前的枚举值做为Field对象,返回.  

自定义grovvy规则

整个规则的执行是通过groovy脚本的方式,目前实现了一些常见方法做为static method,方便客户端调用.  

可参见: com.taobao.tddl.rule.impl.groovy.GroovyStaticMethod

如果用户需要引入自定义的扩展类,可设置extraPackages属性:

<property name="extraPackages">
          <list>
             <value>com.taobao.tddl.rule.extrapackage.AppUtils</value>
          </list>
</property>

规则配置详解

启动tddl规则

spring启动

<bean id="rule" class="com.taobao.tddl.rule.TddlRule" init-method="init">
   <!-- 
      默认为远程规则,如果要强制使用本地规则,可通过设置appRuleFile属性,指向本地的rule定义文件即可
   -->
   <!--
   <property name="appRuleFile" value="V0#classpath:app/tddl-rule.xml"/>
   -->
   <property name="appName" value="TDDL_SAMPLE"/>
</bean>
编程启动
TddlRule rule = new TddlRule();
rule.setAppName("tddl_sample");
// 默认为远程规则,如果要强制使用本地规则,可通过设置appRuleFile属性,指向本地的rule定义文件即可
// rule.setAppRuleFile("V0#classpath:app/tddl-rule.xml");
rule.init(); 

说明

  1. TddlRule默认为使用远程规则
  2. 如果要启用本地规则,需要设置appRuleFile为本地文件
    文件的加载方式支持spring风格的classpath,加载代码使用spring的实现: org.springframework.core.io.support.PathMatchingResourcePatternResolver

规则文件配置

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <!-- 这个bean配置为TDDL规则总配置 -->
  <bean id="vtabroot" class="com.taobao.tddl.rule.VirtualTableRoot" init-method="init">
    <!-- 没有被配置在tableRules的逻辑表都将在这个group里,以单表形式执行 -->
    <property name="defaultDbIndex" value="TDDL_DEFAULT_GROUP"/>  
    <!-- 数据库类型,默认是mysql -->
    <property name="dbType" value="MYSQL"></property> 
    <!-- 该map配置有分表的逻辑表,有几个表有分表就配置几个键值对(该事例表示只有三个表需要分表)-->
    <property name="tableRules">
      <map> 
        <!-- key是逻辑表名,value指的是对应具体配置的id -->
        <entry key="user" value-ref="user_bean"></entry> 
        <!-- 逻辑表名为admin,具体的分表规则在id="admin_bean"的配置中 -->
        <entry key="admin" value-ref="admin_bean"></entry> 
        <!-- 这张表是单表,可以配置在这,不配置的话默认走defaultDbIndex -->
        <entry key="picture" value-ref="picture_bean"></entry> 
      </map>
    </property>
  </bean>

<!-- 按照user_id取模划分64张表,表明具体为'user_0000'-'user_0063', 'user_0000'-'user_0031' 在'TDDL_0000_GROUP'中, 'user_0032'-'user_0063' 在'TDDL_0001_GROUP'中 --> <bean id="user_bean" class="com.taobao.tddl.rule.TableRule"> <!-- groupKey格式框架,{}中的数将会被dbRuleArray的值替代,并保持位数 --> <property name="dbNamePattern" value="TDDL_{0000}GROUP"/> <!-- 具体的分库规则 --> <property name="dbRuleArray"> <!-- 按照user_id取模划分64张表,结果除以32后分到两个库中 --> <value>(#user_id,1,64#.longValue() % 64).intdiv(32)</value> </property> <!-- 具体表名格式框架,{}中的数将会被tbRuleArray的值替代,并保持位数 --> <property name="tbNamePattern" value="user{0000}"></property> <!-- 具体的分表规则 --> <property name="tbRuleArray"> <!-- 按照user_id取模划分64张表 --> <value>#user_id,1,64#.longValue() % 64</value> </property> <!-- 全表扫描开关,默认关闭,是否允许应用端在没有给定分表键值的情况下查询全表 --> <property name="allowFullTableScan" value="true"/> </bean>

<!-- 按照user_id取模划分4张表,表名具体为'admin_00'-'admin_03', 所有表都在'TDDL_DEFAULT_GROUP'中 --> <bean id="admin_bean" class="com.taobao.tddl.rule.TableRule"> <!-- 分库规则,该表没有分库,所有表都在这个group上 --> <property name="dbNamePattern" value="TDDL_DEFAULT_GROUP"/> <property name="tbNamePattern" value="admin_{00}"></property> <property name="tbRuleArray"> <value>#user_id,1,4#.longValue() % 4</value> </property> </bean>

<!-- 即不分库也不分表 --> <bean id="picture_bean" class="com.taobao.tddl.rule.TableRule"> <property name="dbNamePattern" value="TDDL_DEFAULT_GROUP"/> <property name="tbNamePattern" value="picture"></property> </bean> </beans>

说明:

  • 需要定义一个VirtualTableRoot,对应bean id="vtabroot,同时声明init-method="init"
  • 每个逻辑表需要单独定义一个TableRule,每个逻辑表都可以定义自己的分库分表条件.  
  • 每张逻辑表定义需要正式被启用,需要在VirtualTableRoot中的tableRules,定义逻辑表名和规则的映射. 
    比如<entry key="user" value-ref="user_bean"></entry>,这里对外的逻辑表名为user,用户可通过select * from user进行操作. 

表规则定义

每张表规则都需要单独定义TableRule对象,配置如下:

<bean id="admin_bean" class="com.taobao.tddl.rule.TableRule">
//....
</bean>

常用参数列表:

属性名 说明 默认值
dbNamePattern 定义库名:
1. TDDL_SAMPLE_GROUP_{0000} 占位表达式,dbRules计算结果替换{000}
2. TDDL_SAMPLE_GROUP_0  直接库名,不需要替换
无
dbRules

定义分库规则:

例子: 
1.  (#user_id,1,1024#.longValue() % 1024).intdiv(32)

无
tbNamePattern

定义表名:

1. TDDL_SAMPLE_{0000} 占位表达式,tbRules计算结果替换{000}

2. TDDL_SAMPLE_0  直接库名,不需要替换

无
tbRules 定义分表规则:
例子: 
1.  #user_id,1,1024#.longValue() % 1024
无
allowFullTableScan 是否允许进行全表扫描 false
broadcast 是否为广播表或者全局表 false
joinGroup

一个特殊标识
用途:
两张有分库的表进行join时,尝试下推到每个数据库分库上执行join
a.如果有定义joinGroup并且值不同,认为不是同一个join group,不可下推

b.如果未定义joinGroup,则自动通过分库分表结果判断,相同则下推join.

null值
extraPackages groovy rule计算的扩展代码,定义后,static方法可在rule中直接引用 无
dbType 数据库类型,目前暂没用,后续引入其他存储时会启用 MYSQL

常用规则示例

Groovy javaodc :

例子:

  1. 分区字段为Long类型
    分库:  (#user_id,1,1024#.longValue() % 1024).intdiv(32)    // intdiv为groovy中的number的方法
    分表:  #user_id,1,1024#.longValue() % 1024
  2. 分区字段为String类型
    分库: (#user_name,1,10000#.hashcode().abs() % 1024).intdiv(32)  // 注意枚举范围为10000,因为hashcode值会有碰撞,如果只是1024次无法枚举所有的分库分表.
    分表:#user_name,1,10000#.hashcode().abs() % 1024
  3. 分区字段为date类型
    按年天分区: getCalendar(#gmt_create,1_date,366#).get(Calendar.DAY_OF_YEAR)
    按年分区:   yyyy(#gmt_create,1_year,12#)
    按月分区:   mm(#gmt_create,1_month,12#)

使用例子

1.  首先定义rule,可参考规则管理中的配置示例. 

如果需要更多复杂的规则,可参考:ComplexRule

2.  客户端代码

TddlRule rule = new TddlRule();
rule.setAppName("tddl_sample");
rule.init();
// ....
// 计算分库分表
MatcherResult result = rule.route("nserch", "message_id = 1");
//MatcherResult result = rule.route("nserch", "message_id in (996,997,998,999,1000,1001,1002,1003,1004):long");
List<TargetDB> dbs = result.getCalculationResult();
for (TargetDB db : dbs) {
    // 分库遍历
    for (String table : db.getTableNames()) {
        // 分表遍历
        System.out.println(table);
    }
}

Clone this wiki locally