Repository navigation
Tddl_Rule
规则属于TDDL5核心组件之一,其主要用于解决数据的路由问题,将用户SQL定义中的对于一张逻辑表的操作,经过规则计算,可返回需要操作的一张或者多张物理表.
整个流程主要分为三步:
- 构造语法树 【主要为Tddl_Optimizer生成的语法树,经过特定的转化]】
- 基于语法树查找匹配的规则
- 基于规则计算出分库分表的结果
说明:
- 元数据结构比较简单,主要是针对语法树中where条件的处理,每个条件都可以抽象为一组(And/Or)的Comparative组合.
- 做为通用组件,抽象了Comparative语法树构造器,允许tddl optimizer基于ASTNode实现,也允许外部用户基于自己特定的结构进行生成.
默认支持的构造器:
- 基于TDDL5 Optimizer的ASTNode,可查看:OptimizerRule相关使用Tddl-Rule组件的代码
- 基于string文本解析
简单语法: KEY CMP VALUE [:TYPE]
- KEY: 类似字段名字,用户随意定义
- CMP: 链接符,比如< = > 等,具体可查看Comparative中的定义
- VALUE:对应的值,比如1
- TYPE: 描述VALUE的类型,可选型,如果不填默认为Long类型。支持: int/long/string/date,可以使用首字母做为缩写,比如i/l/s/d
几个例子:
- id = 1
- id = 1 : long
- id > 1 and id < 1 : long
- gmt_create = 2011-11-11 : date
- id in (1,2,3,4) : long
说明:
- TddlRuleConfig做为tddl-rule组件的整个上下文,允许同时存在多个版本. (比如配合数据做迁移时,动态切换规则的那一段状态)
- 每个Tddl规则库,都是一个VirtablTableRoot,其管理着多个VirtualTableRule.
- 每个VirtualTableRule对应一张逻辑表的规则定义,库规则和表规则,都可以进行定义,定义通过Rule对象来描述.
- 每个Rule对象,包含多个RuleColumn,也就是一个分库分表规则可以支持多个字段.
- 默认的Rule是先是基于Groovy动态脚本定义,以及支持按照多种类型的分库实现。默认支持按照id做hash,或者按照时间做滚动(多种时间纬度,比如按小时,天,月,年等)
规则的定义是整个tddl-rule中最难理解的一部分,所以单独展开来介绍,帮助大家更快理解和上手.
在介绍之前,首先需要思考几个问题:
- 规则中配置都是基于xml,字符串解析后,如何来描述数据的类型,比如这个字段是date类型,需要按照时间对象进行处理.
- sql查询中会有全库扫描的需求,如何通过规则来进行描述整个数据库的分库分表的拓扑结构.
tddl中会解决这两个问题,特殊设计了一种表达式:#user_id,1_number,4#
解释一下:
a. 整个表达式通过前后两个#来区分不同的字符串表达式,表达式解析中,会通过#(.*)#进行正则匹配提取.
b. 表达式中的一个分为3个部分:
- user_id表示分表字段名
- 1_number标示当前的数据自增步长和类型(下划线_做为分隔符),number对应于AtomIncreaseType中的类型. (支持number,hour,date,month,year)这几类. 如果是1_hour,那代表是按1小时进行自增.
- 最后一个数字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为了要知道的是它需要在哪些表上执行
- 通过解析sql之后的到的参数名是'user_id',并且comparative参数值是'> 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#中第三个参数的值,那么停止枚举. - 经过枚举,最终得到的表为: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为了要知道的是它需要在哪些表上执行
- 通过解析sql之后的到的参数名是'gmt_create',并且comparative参数值为'>2013-12-24 and gmt_create <= 2013-12-31'
- 因为'>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中的范围值进行判断. - 经过枚举,最终得到的表为:log_25,log_26,log_27,也就是查询这三个日期内对应表的数据.
规则目前是通过spring bean xml来定义Rule,选择spring主要的考虑是减少大家熟悉成本,没有特别的因素.
目前spring只管理Rule对象的创建和销毁,规则计算过程中,不会涉及和spring的相关调用.
规则的配置方式:
- 本地规则
- 远程规则 【建议使用,支持diamond运行时推送,客户端可即时生效,可解决数据扩容时对应用的无缝切换,远程更新规则,客户端不需要发布和重启】
规则计算主要是针对VirtualTableRule对象,获取其定义的DbRules和TbRules,根据当前Comparative语法树中的参数进行组合计算.
通过规则定义的章节,已经向大家展示了基本的规则的用法,但实际规则计算中涉及的会相对复杂,复杂度增加的几个因素:
- 分库/分表同时存在
- 多个分库字段
- 可选分库分表字段【比如一个表是按照两个字段进行分库分表,如果只提供一个字段时,需要进行部分表的扫描】
- in分库分表枚举值追踪 【需要记录产生这个分库分表结果的枚举值,比如可通过user_id = 1,计算出表在admin_01上,那id in (1,2),会根据计算出表的不同,发到admin_01上的条件就会变为id in (1),admin_02上的条件就会变为id in (2),这样的优化在对于id in的条件数比较多时非常有效】
- 自定义grovvy规则
在介绍规则计算之前,首先要了解规则计算中一个最重要的概念:Samples(枚举值或称描点).
Samples可以理解为一个排列组合枚举中的一种组合的记录,遍历完整个排列组合后,可以得到一组Samples.
比如:id in (1,2) and name in ('ljh','hlh'), id = 1 and name = 'ljh' 就是一个排列组合的值,就是一个Samples.
了解了基本概念,再来看一下规则计算的流程:
- 首先根据ComparativeMapChoicer和当前VirtualTableRule中的Rule定义,找到匹配的分库和分表的Rule. 【一个表允许同时存在多份Rule,根据对应的参数进行自动匹配Rule,运行时选择】
- 生成分库分表字段的枚举值列表. 【比如分库字段A的枚举值,分库字段B的枚举值,最终的A,B一起的枚举值就是A * B组合的一个笛卡尔积】
- 遍历枚举值列表,调用Rule.calculate()进行计算. 【目前Rule计算的实现使用Groovy】
比如分表Rule定义: #id,1_number,1024# % 4 ,会被编译编译为Groovy的脚本,对应的#id,1_number,1024#会被替换为map.get("id")的调用. 而对应的map就是枚举值对象。所以每个枚举值对象都会计算出一个结果,遍历完所有的枚举值后进行去重,所得的结果就是分库分表的结果.
目前支持规则计算的操作符:
- GreaterThan(>)
- GreaterThanOrEqual(>=)
- Equivalent(=)
- NotEquivalent(!=)
- LessThan(<)
- LessThanOrEqual(<=)
sql例子:
- where id = 1 【直接hash计算】
- where id > 1 【半开区间,会是个全库扫描】
- where id >= 1 【半开区间,会是个全库扫描】
- where id < 5 【半开区间,会是个全库扫描】
- where id <=5 【半开区间,会是个全库扫描】
- where id > 1 and id < 4 【部分区间枚举】
- where id >=1 and id <=4 【部分区间枚举,between 1 and 4可转化为此模式】
- where id = 1 or id = 2 or id =3 【批量hash计算,id in (1,2,3)可转化为此模式】
- 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'中
此时的枚举逻辑会有所变化,枚举算法:
- 首先按照分库的Rule规则进行计算,同时记录每个分库的计算结果对应的枚举值 【比如计算出TDDL_0000_GROUP,对应的枚举值为0~31.】
- 然后基于每个分库的计算结果对应的枚举值,再按照分表的Rule规则进行枚举计算,最后得出整个的分库分表规则
总结需要额外考虑问题:
- 分库分表有无公共字段 【无影响,各自算各自的】
- 分库分表有公共字段 【有影响,先算库规则,基于库规则的枚举值,再算表规则】
a. 库规则和表规则的步长,类型,范围值都一样
b. 库规则和表规则的步长,类型,范围值存在不一样【比如库规则是1_month,按照月进行分库,表规则是1_date,按照天进行分表】
相关需求:
- 日志按照天进行划分,数据量还过大,需要再按照id进行hash分表. 对应的规则可能就是 #gmt_create,1_date,31# % 32 + "_" + #id,1_number,4# % 4.
此时的枚举逻辑会有所变化,枚举算法:
- 各自计算出单个字段的枚举值列表
- 将多个字段的枚举值列表进行全排列算法,得出每一种组合的可能性. 核心代码类Samples.java
- 按照分库/分表枚举算法,得出枚举值列表.
相关需求:
- 全库扫描 【不提供分库字段的条件】
- 多字段部分表扫描 【比如日志按照时间+id hash进行分库分表,如果用户需要查询每条id的记录时,需要对时间范围进行枚举遍历】
可选配置:(字段信息后加个?代表optional)
#column?,1_number,1024
此时的枚举逻辑会有所变化,枚举算法:
- 计算出存在字段条件的枚举值列表
- 针对不存在字段条件的,按照Rule定义中的值进行枚举. 定义:#gmt_create,1_date,31#
- 按照多个分库字段的枚举算法,得出最终的枚举值列表.
相关需求:
- in的批量查询
- tddl5中join模式的Index Loop Join
目前tddl中对于in的支持是转化为OR模式,比如id in (1,2,3) 会转化为 id = 1 or id = 2 or id = 3.
此时的枚举逻辑会有所变化,枚举算法:
- 计算出条件的枚举值列表
- 调用rule进行运算得出分库分表值,同时记录当前的枚举值做为Field对象,返回.
整个规则的执行是通过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>
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();
说明
- TddlRule默认为使用远程规则
- 如果要启用本地规则,需要设置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 |
定义分库规则: 例子: |
无 |
| tbNamePattern |
定义表名: 1. TDDL_SAMPLE_{0000} 占位表达式,tbRules计算结果替换{000} 2. TDDL_SAMPLE_0 直接库名,不需要替换 |
无 |
| tbRules | 定义分表规则: 例子: 1. #user_id,1,1024#.longValue() % 1024 |
无 |
| allowFullTableScan | 是否允许进行全表扫描 | false |
| broadcast | 是否为广播表或者全局表 | false |
| joinGroup |
一个特殊标识 b.如果未定义joinGroup,则自动通过分库分表结果判断,相同则下推join. |
null值 |
| extraPackages | groovy rule计算的扩展代码,定义后,static方法可在rule中直接引用 | 无 |
| dbType | 数据库类型,目前暂没用,后续引入其他存储时会启用 | MYSQL |
Groovy javaodc :
- number http://groovy.codehaus.org/groovy-jdk/java/lang/Number.html
- string http://groovy.codehaus.org/groovy-jdk/java/lang/String.html
例子:
- 分区字段为Long类型
分库: (#user_id,1,1024#.longValue() % 1024).intdiv(32) // intdiv为groovy中的number的方法
分表: #user_id,1,1024#.longValue() % 1024 - 分区字段为String类型
分库: (#user_name,1,10000#.hashcode().abs() % 1024).intdiv(32) // 注意枚举范围为10000,因为hashcode值会有碰撞,如果只是1024次无法枚举所有的分库分表.
分表:#user_name,1,10000#.hashcode().abs() % 1024 - 分区字段为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);
}
}


