-
Notifications
You must be signed in to change notification settings - Fork 717
分库分表规则原理及自定义配置
业务在使用分表分表时多数会使用简单的hash分表或者按照时间或者id使用内置的range分表函数,但某些情况下这些简单的hash规则和内置函数并不能满足业务复制的分表场景,这时就需要业务自定义分库分表规则。而zebra的分库分表规则使用的是groovy脚本,理论上可以支持定制各种复杂的路由规则。
首先,先看一个简单的分库分表规则(使用本地配置时的XML),后面会基于该例子解释zebra分库分表路由的原理:
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="ShardBasicOperation" global="false" generatedPK="Id">
<shard-dimension dbRule="#Uid#%2"
dbIndexes="zebratestservice[0-1]"
tbRule="(#Uid#).intdiv(2)%4"
tbSuffix="everydb:[0,3]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
对于table-shard-rule、shard-dimension、dbIndexes的解释可以参考Zebra分库分表接入指南。
ShardDataSource在启动时会解析物理库索引(dbIndexes)和表名后缀(tbSuffix),然后生成一个库和表的链表(关于dbIndexes和tbSuffix的配置规则可以参考分库分表规则dbIndexes及tbSuffix配置和Zebra分库分表接入指南。比如在上面的例子中,解析**zebratestservice[0-1]**会得到zebratestservice0和zebratestservice1两个分库的JdbcRef(如果使用本地配置,这里是dataSourcePool里的key)。对于everydb[0,3],是指在每个分库里都有后缀为0-3的4张表(如果是alldb:[0-3],则每个库上只有1个表,具体请看 Zebra分库分表接入指南。所以解析完库和表的配置后可以得到这样一个链表:
一共zebratestservice0和zebratestservice1两个库,每个库都有ShardBasicOperation0-ShardBasicOperation3四张表。
对于库和表的路由规则,ShardDataSource会生成一个GroovyObject,比如对于上面的#Uid#%2和(#Uid#).intdiv(2)%4,会动态生成两个用于路由的对象,并且会将#shardKey#替换成从参数map查找的对应字段:
// dbRule
class RuleEngineBaseImpl extends RuleEngineBase{
Object execute(Map context) {
context.get("Uid")%2
}
}
// tbRule
class RuleEngineBaseImpl extends RuleEngineBase{
Object execute(Map context) {
context.get("Uid").intdiv(2)%4
}
}
简单的说,ShardDataSource在进行路由时会根据库和表的路由规则计算出一个索引(注意:这里是索引!不是后缀!!),然后拿这个索引到库和表的数组中寻找对应下标的物理库和物理表,在进行表的路由时,最终算的是某个确定的库上的第几个表,因此在tbRule的配置里,算的是根据dbRule找中的某个库中的某个表的索引。
例如当Uid = 5时,#Uid#%2=1,(#Uid#).intdiv(2)%4=2,所以会路由到zebratestservice1的表ShardBasicOperation2中。
了解了ShardDataSource路由的原理后,我们就可以很容易的根据需求定制路由规则。
注意:
- shardByMonth、shardById等内置range分表函数与普通规则的配置略有不同,但基本原理类似,只是函数返回的是map,将dbRule和tbRule的结果整合到一起。可以参考zebra range的分库分表配置。
- 在计算tbRule之前,因为已经根据dbRule找到了对应库,所以tbRule的计算结果是对应分库中的表的index,不是所有表的,注意表的下标范围。
检查规则计算结果
针对上面demo中的规则,可以使用下面代码测试规则的计算结果(db或tb的索引值,根据这个索引查找对应的库和表):
RuleEngine dbRule = new GroovyRuleEngine("#Uid#%2");
RuleEngine tbRule = new GroovyRuleEngine("(#Uid#).intdiv(2)%4");
Map<String, Object> valMap = new HashMap<String, Object>();
valMap.put("Uid", 123);
System.out.println("dbIndex = "+dbRule.eval(valMap));
System.out.println("tbIndex = "+tbRule.eval(valMap));
检查dbIndex及tbSuffix配置
// 在配置好规则后可以执行下面的代码,会打印出规则中配置的所有的库及每个库中的表
ShardDataSource ds = new ShardDataSource();
ds.setParallelExecuteTimeOut(10000);
ds.setRuleName("zebra-test-service");
ds.init();
Map<String, TableShardRule> shardRules = ds.getRouter().getRouterRule().getTableShardRules();
for (Map.Entry<String, TableShardRule> entry1 : shardRules.entrySet()) {
System.out.println("================================================================================");
List<DimensionRule> dimensionRules = entry1.getValue().getDimensionRules();
System.out.println("逻辑表名: " + entry1.getKey() + ", 共"+dimensionRules.size()+"个维度\n");
for (int i = 0; i < dimensionRules.size(); ++i) {
DimensionRule dimensionRule = dimensionRules.get(i);
Map<String, Set<String>> allDBAndTables = dimensionRule.getAllDBAndTables();
System.out.println("是否主维度: "+dimensionRule.isMaster()+", 是否范围分表: "+dimensionRule.isRange()
+", 是否需要辅维度同步: "+dimensionRule.needSync() + ", 共" + allDBAndTables.size() + "个分库");
for (Map.Entry<String, Set<String>> entry2 : allDBAndTables.entrySet()) {
System.out.println("库: "+entry2.getKey());
System.out.println("表("+entry2.getValue().size()+"): "+entry2.getValue());
}
if(dimensionRules.size() > 1 && i < dimensionRules.size()-1)
System.out.println("--------------------------------------------------");
}
}
实例
a. 有两个库testdb0、testdb1要对表TestTable按Id进行分表,Id<=10000的会落到testdb0上,hash到4个分表上,10000<Id<=50000的落到testdb1上,hash到8个分表上,Id > 50000的落到testdb1的第9张表(默认表,和逻辑表名相同)。
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="TestTable" global="false" generatedPK="Id">
<shard-dimension
dbRule="def result; if (#Id# <= 10000) { result = 0; } else { result = 1; }; return result;"
dbIndexes="testdb[0-1]"
tbRule="def result; def param = #Id#; if (param <= 10000) { result = param % 4; } else if (param <= 50000) { result = param % 8; } else { result = 8; }; return result;"
tbSuffix="testdb0:{0,4}&testdb1:{0,8}&testdb1:[$]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
b. 库testdb要对表TestTable按Id进行分表但不分库,按照Id均匀hash到4个分表上。
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="TestTable" global="false" generatedPK="Id">
<shard-dimension
dbRule="#Id#==null?0:0"
dbIndexes="testdb"
tbRule="#Id#.intValue()%4"
tbSuffix="alldb:[0,3]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
c. 库testdb要对表TestTable按Id进行分库但每个库中不分表,按照Id均匀hash到4个库上(testdb0 – testdb3),每个库中的物理表都是TestTable0。
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="TestTable" global="false" generatedPK="Id">
<shard-dimension
dbRule="#Id#.intValue()%4"
dbIndexes="testdb[0-3]"
tbRule="#Id#==null?0:0"
tbSuffix="everydb:[0,0]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
eg:
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="welife_users" global="false" generatedPK="uid">
<shard-dimension dbRule="#uid#%8"
dbIndexes="welife[0-7]"
tbRule="(#uid#.intdiv(8))%16"
tbSuffix="alldb:[0,127]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
dbIndexes主要用于配置分库,ShardDataSource初始化时会根据配置解析出一个库的列表,在SQL路由时根据dbRule算出index,到列表内查找对应位置的db。
dbIndexes配置中指定所有分库的GroupDataSource的jdbcRef(如果使用平台配置的规则,这里的jdbcRef是真实分库的JdbcRef,如果使用本地XML配置,这里指DataSourcePool里对应的key)。例如上面例子中的dbIndexes可以有以下几种等价的写法,它们的index都是从0开始按照写的顺序呢进行排列:
1. welife[0-7] // 注意中间是‘-’,是‘,’
2. welife0,welife1,welife2,welife3,welife4,welife5,welife6,welife7 // 多个JdbcRef间用‘,’分隔
3. welife0,welife[1-6],welife7
是表的后缀命名规则,在逻辑表名后面加上配置的后缀得到所有物理分表的表名,目前有alldb和everydb及自定义配置。
1.alldb
每个分库上分表数量相同,但后缀递增。例如上面配置中分8个库,每个库16张表,逻辑表名为welife_users,则对应物理表:
alldb:[0,127] // 注意与dbIndexes不同,括号内用’,‘分隔而非’-‘
// 对应的库和表如下
welife0: welife_users0,welife_users1,welife_users2, ......, welife_users15
welife1: welife_users16,welife_users17,welife_users18, ......, welife_users31
......
welife7: welife_users112,welife_users113,welife_users114, ......, welife_users127
2.everydb
与alldb类似,但每个分库中表名后缀是相同的
everydb:[0,15] // 注意与dbIndexes不同,括号内用’,‘分隔而非’-‘
// 对应的库和表如下
welife0: welife_users0,welife_users1,welife_users2, ......, welife_users15
welife1: welife_users0,welife_users1,welife_users2, ......, welife_users15
......
welife7: welife_users0,welife_users1,welife_users2, ......, welife_users15
3.自定义配置
目前自定义配置支持两种形式:jdbcRef:[a,b,c...]和jdbcRef:{suffix0,sufix9}(若是平台配置的规则,jdbcRef应使用真实库的JdbcRef,若是本地XML配置的规则,jdbcRef应使用对应的key)
a. jdbcRef:[a,b,c...] ===> logicalTable+a, logicalTable+b, logicalTable+c, ......。中括号内可以配置$,表示分表名和逻辑表名相同 。例如某规则中有testdb01这个JdbcRef,逻辑表为testTable, 对应库上的tbSuffix配置为 testdb01:[_0,_1,_2],解析后的结果:
testTable_0,testTable_1,testTable_2
b. jdbcRef:{suffix0,suffix9} 批量配置,生成表格式:logicalTable+suffix+index0, logicalTable+suffix+index1, ......
注意:这种批量配置无法区分日期,比如配置是jdbcref:{_201801,_201902},会解析成102张表而不是按月份排列的14张表!!
eg. 逻辑表名为abc,ref1和ref2表示规则中dbIndexes里配的某个JdbcRef(若是本地配置应为DataSourcePool中配置的key)
配置 | 解析后的表 |
---|---|
ref1:[_a,_b,_c] | 库1(ref1):abc_aa, abc_bb,abc_c |
ref1:[0,2] | 库1(ref1):abc0, abc2 |
ref1:[_201501,_201502]&ref2:[_201503] | 库1(ref1): abc_201501, abc_201502 库2(ref2): abc_201503 |
ref1:[_0,_1]&ref2:[$,_3] | 库1(ref1): abc_0, abc_1 库2(ref2): abc, abc_3 |
ref1:[$] | 库1(ref1): abc |
ref1:[_0,_1,_3]&ref1:[$] | 库1(ref1): abc_0, abc_1, abc_3, abc |
ref1:{_bak0,_bak3} | 库1(ref1):abc_bak0, abc_bak1, abc_bak2, abc_bak3 |
ref1:{_201801,_201803}&ref2:{_201901,_201902} | 库1(ref1):abc_201801, abc_201802, abc_201803 库2(ref2):abc_201901, abc_201902 |
4.多个配置混合使用
alldb或everydb可以与自定义配置混合使用,顺序解析,规则间以&分隔,表的顺序以配置为准。
eg. 假如有两个库ref1和ref2(dbIndexes: ref1,ref2),且逻辑表名为abc
示例 | 结果 |
---|---|
ref1:[$]&everydb:[_0,_1] | 库1(ref1): abc, abc_0, abc_1 库2(ref2): abc_0, abc_1 |
ref1:[$]&alldb:[_0,_3] | 库1(ref1): abc, abc_0, abc_1 库2(ref2): abc_2, abc_3 |
ref1:[$,_x0]&everydb:[_0,_1] | 库1(ref1): abc, abc_x0, abc_0,abc_1 库2(ref2): abc_0, abc_1 |
everydb:[_0,_1]&ref1:[$] | 库1(ref1): abc_0, abc_1, abc 库2(ref2): abc_0, abc_1 |
alldb:[_0,_3]&ref1:[$] | 库1(ref1): abc_0, abc_1, abc 库2(ref2): abc_2, abc_3 |