当前位置: 首页 > 文档资料 > Zebra 中文文档 >

分库分表规则原理及自定义配置

优质
小牛编辑
118浏览
2023-12-01

背景

业务在使用分表分表时多数会使用简单的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),然后生成一个库和表的链表(关于dbIndexestbSuffix的配置规则可以参考分库分表规则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的第9张表(默认表,和逻辑表名相同)。</id

<?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配置

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

tbSuffix配置

是表的后缀命名规则,在逻辑表名后面加上配置的后缀得到所有物理分表的表名,目前有alldbeverydb自定义配置

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