Skip to content

Zebra分库分表接入

junior_xin edited this page Apr 3, 2019 · 10 revisions

1 介绍

该文档主要介绍Zebra分库分表ShardDataSource的接入和使用,主要包括分库分表的背景知识、ShardDataSource的配置、分库分表规则的配置等。

2 准备

2.1 背景介绍

在一个业务刚上线时,可能使用某个单表存储数据。随着时间的推移和用户的增加,单表内的数据量会不断变大,总有一天数据量会大到一个难以处理的地步。这时仅仅一张表的数据就可能过亿甚至更多,无论是查询还是修改,对于它的操作都会很耗时,这时就需要考虑是否对数据库或表进行切分了。通过一系列的切分规则将数据水平分布到不同的库或表中,在通过相应的库路由或者表路由规则计算需要查询的具体的库表,以进行增删改操作(这里所说的拆分通常指水平切分),可以参考Zebra分库分表介绍

3 数据源配置

3.1 添加依赖

依赖版本请参考Zebra版本更新说明

<dependencies>
	<!-- 数据源 -->
	<dependency>
		<groupId>com.dianping.zebra</groupId>
		<artifactId>zebra-client</artifactId>
                <version>${version}</version>
	</dependency>
	<!-- CAT监控-->
	<dependency>
		<groupId>com.dianping.zebra</groupId>
		<artifactId>zebra-cat-client</artifactId>
                <version>${version}</version>
	</dependency>
<dependencies>

3.2 增加数据源配置

添加Spring配置
local模式
a.配置分库的DataSource。 下面的例子,定义好所有的分库后,最后使用ShardDataSource把以上所有定义的DataSource串起来。

<bean id="zebraDs0" class="com.dianping.zebra.group.jdbc.GroupDataSource" init-method="init" destroy-method="close">    
  <property name="jdbcRef" value="test" />    
  ......
</bean>
<bean id="zebraDs1" class="com.dianping.zebra.single.jdbc.SingleDataSource" init-method="init" destroy-method="close">  
    ......
</bean>
<bean id="zebraDs2" class="com.dianping.zebra.single.jdbc.SingleDataSource" init-method="init" destroy-method="close">  
  ......
</bean>
<!-- ShardDatasource接入配置 -->
<bean id="zebraDS" class="com.dianping.zebra.shard.jdbc.ShardDataSource" init-method="init">  
  <property name="dataSourcePool">    
    <map>      
      <entry key="id0" value-ref="zebraDs0"/>      
      <entry key="id1" value-ref="zebraDs1"/>      
      <entry key="id2" value-ref="zebraDs2"/>         
    </map>    
  </property>  
  <property name="routerFactory">    
    <bean class="com.dianping.zebra.shard.router.builder.XmlResourceRouterBuilder">        
      <constructor-arg value="spring/shard/router-local-rule.xml"/>    
    </bean>  
  </property>  
  <property name="parallelCorePoolSize" value="16" />  
  <property name="parallelMaxPoolSize" value="32" />  
  <property name="parallelWorkQueueSize" value="500" />  
  <property name="parallelExecuteTimeOut" value="3000" />
</bean>

b.分表规则配置(spring/shard/router-local-rule.xml)

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>  
  <table-shard-rule table="tb">    
    <shard-dimension dbRule="#id#%4" dbIndexes="db[0-3]" tbRule="#id#.intdiv(4)%2" tbSuffix="alldb:[0,7]" isMaster="true">    
    </shard-dimension>  
  </table-shard-rule>
</router-rule>

zookeeper模式
rulename需要先在管理平台创建好才可使用

<bean id="zebraDS" class="com.dianping.zebra.shard.jdbc.ShardDataSource" init-method="init">
	<property name="ruleName" value="${ruleName}" />
	<property name="configManagerType" value="zookeeper" />
	<property name="parallelCorePoolSize" value="16" />
	<property name="parallelMaxPoolSize" value="32" />
	<property name="parallelWorkQueueSize" value="500" />
	<property name="parallelExecuteTimeOut" value="3000" />
</bean>

3.3 增加监控配置

这个类似GroupDataSource的监控配置部分,参考Zebra读写分离接入指南

4 规则配置

zebra的分库分表规则使用的是groovy脚本,理论上可以支持定制各种复杂的路由规则。在配置分表规则前,用户可以参考如何选择合适的分表键、路由规则、分片数,了解下分库分表不同类型规则的优缺点及适用范围、如何确定合适的分库分表数量等等。

4.1 配置详细说明

zebra分库分表规则目前支持平台配置或本地配置。但不管是那种配置,配置内容基本一致,我们先以下面一个配置大体说明下配置中每个属性的功能。

本地配置XML:

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>  
  <table-shard-rule table="db_users">    
    <shard-dimension dbRule="#uid#%8" dbIndexes="user_db[0-7]" tbRule="(#uid#.intdiv(8))%16" tbSuffix="alldb:[0,127]" isMaster="true">    
    </shard-dimension>  
  </table-shard-rule>
</router-rule>

上面例子中对user_db进行分库分表,分8个库,每个库16张分表,共128张表。

router-rule:router-rule下可以有多个表的分库分表规则。

  table-shard-rule: 每个table-shard-rule表示一个表的分库分表规则。

  • table属性表示逻辑表名(user_db就是在SQL中将要使用的表名)的分表配置

shard-dimension: 分库分表维度,每个维度中配置一个按某些字段(shardkey)进行分表的规则。每个表的规则可以有多个维度规则,该例中仅有单维度。

  • isMaster: 主维度标志,如果只有一个维度,isMaster设成true。

  • 是否需要同步:辅维度中的同步标识,如果辅维度需要同步则设成true(需要同步的辅维度目前仅支持平台配置且辅维度同步需要另配解析及同步服务)。

  • 主维度和辅维度:主维度是默认可写的维度,每个表的规则中只能有一个主维度。多数情况下可能只需要一个维度,zebra-client在启动的时候解析规则配置,如果只配了一个维度,不管这个维度是否设置了isMaster都会默认设置成主维度(建议只有一个维度也设置成主维度)。有时候只有主维度可能无法满足用户的查询需求,比如某些场景下业务设置主维度根据UserId进行分表,但是有时查询的时候SQL不会带UserId而是根据OrderId查询,因为没有UserId的值,zebra-api无法进行准确的路由而只能全表扫描,性能会受到比较大的影响,这时候就可能用到辅维度。辅维度的配置有两种情况:一是主维度和辅维度用的实际是同一张物理表(比如辅维度分表键实际包含了主维度分表键),即根据主维度规则进行路由和根据辅维度规则进行路由的结果是完全相同的一张物理表、映射到同一行上,这种情况下可以只添加一个辅维度配置即可。二是根据主维度规则进行路由和根据辅维度规则进行路由的结果完全不同,这时候主维度和辅维度在物理上是不同的两套表,需要配置同步任务,将主维度的数据以binlog的形式同步到辅维度上,简单的说就是拿到主维度的数据然后根据辅维度的规则再写入到另一张表里,查询的时候如果是根据主维度分表键查就会到主维度的表里查,如果是根据辅维度的分表键查就会落到辅维度的表里。因为同步会有一定的延迟,需要谨慎使用,目前多用于运营库。

  • dbRule: 指定库名的路由规则表达式。zebra会解析出SQL中#uid#维度和值,并将该值带入该表达式计算出最终落到的数据库的index。支持多个字段组成的单维度。其中dbRule是groovy脚本,理论上可以支持任何逻辑(注意:dbRule和tbRule算出的是个索引而非后缀!zebra路由的基本原理及自定义规则配置可以参考 分库分表规则原理及自定义配置)

  • dbIndexes: 指定所有拆分的库的GroupDataSource的jdbcRef(如果使用远程配置的规则,这里的jdbcRef是真实分库的JdbcRef,如果使用本地XML配置,这里指DataSourcePool里对应的key)。zebra会将计算出的index带入该表达式,得出最终落到的数据库的jdbcRef值。dbIndexes可以有以下几种等价的写法,它们的index都是从0开始按照写的顺序呢进行排列。(参考 分库分表规则dbIndexes及tbSuffix配置)

  • dbIndexes: 指定所有拆分的库的GroupDataSource的jdbcRef(如果使用远程配置的规则,这里的jdbcRef是真实分库的JdbcRef,如果使用本地XML配置,这里指DataSourcePool里对应的key)。zebra会将计算出的index带入该表达式,得出最终落到的数据库的jdbcRef值。dbIndexes可以有以下几种等价的写法,它们的index都是从0开始按照写的顺序呢进行排列。(参考 分库分表规则dbIndexes及tbSuffix配置)

user_db[0-7]         user_db0,user_db1,user_db2,user_db3,user_db4,user_db5,user_db6,user_db7 
                     welife0,welife[1-6],welife7
  • tbRule: 指定是分表的路由规则表达式。zebra会解析出SQL中#uid#维度和值,并将该值带入该表达式计算出最终落到的数据库中表名的后缀index。支持多个字段组成的单维度。例如下面的表达式,zebra会解析SQL,得到#bid#和#uid#两个值带入表达式得出index。其中bRule都是groovy脚本,理论上可以支持任何逻辑。
(crc32(md5(#bid# + "_" + #uid#))).toLong()%10
  • tbSuffix: 是表的后缀命名规则,解析后会生成一个物理表名的链表。

注意

1.如果是在平台上配置,为了防止业务误操作带来的影响,分库分表规则修改后需要重启应用机器才会生效。

4.2 分库分表路由的基本原理

ShardDataSource在启动时会解析物理库索引(dbIndexes)和表名后缀(tbSuffix),然后生成一个库和表的链表。比如在上面的例子中,解析welife[0-7]会得到welife0、welife1、......、welife7共8个分库的JdbcRef(如果使用本地配置,配置里应是dataSourcePool里的key)。对于alldb[0,127],是指在8个分库中每个库都有16张分表,分表后缀递增。所以解析完库和表的配置后可以得到这样一个链表: 一共welife0、welife1、......、welife7等8个分库,每个库都有16张表,表名后缀按库递增。

对于库和表的路由规则,ShardDataSource会各生成一个对应GroovyObject,比如对于上面的#uid#%8和(#uid#).intdiv(8)%16,会动态生成两个用于路由的对象,并且会将#uid#替换成从参数map查找的对应字段:

// dbRuleclass 
RuleEngineBaseImpl extends RuleEngineBase{  
  Object execute(Map context) {    
    context.get("uid")%8  
  }
}
// tbRuleclass 
RuleEngineBaseImpl extends RuleEngineBase{  
  Object execute(Map context) {    
    context.get("uid").intdiv(8)%16  
  }
}

简单的说,ShardDataSource在进行路由时会根据库和表的路由规则计算出一个索引(注意:这里是索引,不是后缀),然后拿这个索引到库和表的链表中寻找对应下标的物理库和物理表,在进行表的路由时,最终算的是某个确定的库上的第几个表,因此在tbRule的配置里,计算结果是根据dbRule找中的某个库中的某个表的索引。例如当uid = 17时,#uid#%8=1,(#uid#).intdiv(8)%16=2,所以会路由到welife1的表welife_users18中。 了解了ShardDataSource路由的原理后,我们就可以很容易的根据需求定制路由规则,具体可以参考分库分表规则原理及自定义配置。 注意:如果使用shardByMonth及shardById等内置range分表函数配置略有不同,但基本原理类似,可以参考Range方式分片

4.3 分库分表的Join

一般情况下,分库分表后,就不能再和单表一样进行Join了,目前zebra支持以下两种情况的Join。

4.3.1 小表广播

适用在一些配置表,或者一般不怎么变更的小表上,然后分表需要和这个小表进行Join。对于小表需要在每个分库上复制一个,所有对这张表的Join就会变成单库Join了。另外,对于这张表的任何变更,zebra后端会使用binlog的方式自动同步到每一个分库上去。所以这种方式叫做小表广播。

注意:小表广播需要自行配置同步服务!

4.3.2 Binding Table

适用在若干个分表上进行Join,前提是这些分表的分表逻辑都是一样的,意味着所有的Join都可以在同一个数据库上进行。

比如:表a和表b同时都要分表,而且都是使用UserID进行分表,分表的个数和分库的个数都是一样的。

4.4 常见案例

本地化配置例子

a. DataSource配置:

<bean id="gds0" class="com.dianping.zebra.group.jdbc.GroupDataSource" destroy-method="close" init-method="init">  
  <property name="jdbcRef" value="testdb0"/>  <property name="poolType" value="hikaricp"/>  
  ...</bean>
<bean id="gds1" class="com.dianping.zebra.group.jdbc.GroupDataSource" destroy-method="close" init-method="init">
  <property name="jdbcRef" value="testdb1"/>
  <property name="poolType" value="hikaricp"/>  
  ...</bean> 
<bean id="shardds" class="com.dianping.zebra.shard.jdbc.ShardDataSource" init-method="init" destroy-method="close">   
  <property name="dataSourcePool">      
    <map>     
      <!-- 注意:dataSourcePool中配置的key为小写且全局唯一的 -->         
      <entry key="id0" value-ref="gds0"/>          
      <entry key="id1" value-ref="gds1"/>      
    </map>    
  </property>   
  <property name="routerFactory">      
    <bean class="com.dianping.zebra.shard.router.builder.XmlResourceRouterBuilder">         
      <constructor-arg value="xxx/router-rule.xml"/>      
    </bean>   
  </property>   
  <!--业务自行调整并发查询的线程池参数 -->   
  <property name="parallelCorePoolSize" value="16" />   
  <!--业务自行调整并发查询的线程池参数 -->   
  <property name="parallelMaxPoolSize" value="32" />   
  <!--业务自行调整并发查询的线程池参数 -->   
  <property name="parallelWorkQueueSize" value="500" />   
  <!--业务自行调整逻辑SQL在线程池里面的超时时间,可以在beta环境设置的大一点 -->   
  <property name="parallelExecuteTimeOut" value="3000" />
</bean>

注意:分库的数据源可以使用GroupDatasource或SingleDataSource,dataSourcePool中key必须为小写且全局唯一。

b. 规则配置 (router-rule.xml)

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>  
  <table-shard-rule table="TestTable">    
    <shard-dimension dbRule="#Id#.intdiv(2)%2"
                     <!--如果是本地配置的规则,dbIndexes内用的是dataSourcePool的key-->      
    		     dbIndexes="id[0-1]"         
    		     tbRule="#Id#%2"       
    		     tbSuffix="everydb:[0,1]"      
    		     isMaster="true">    
    </shard-dimension>  
  </table-shard-rule></router-rule>

5.注意事项!!

数据源配置相关

1.配置中有关并发的等属性(parallelxxx)是什么意思?

ShardDataSource配置中以parallel开头的属性主要用于修改SQL执行时线程池的配置(注意和连接池的区别)。该线程池是在ShardDataSource执行SQL时,如果路由结果不是单表而是多个库或多个表,这时会将SQL扔到线程池里执行,默认按库并行,单库内SQL串行。具体参数需要业务根据实际情况自行调整。

2.如何在ShardDataSource上配置连接池属性?

在2.9.8及以上版本中,ShardDataSource里可以配置连接池属性,属性名称及配置方式与GroupDatasource形同(zebra接入指南),ShardDataSource会将参数传递给内部所有GroupDatasource。

3.如何避免全表扫问题?

对于select、update及delete语句,若SQL中没有带shardKey的where条件同时没用API的方式路由或shardKey是大于小于等无法进行精确路由的条件(range分表略有不同),这时会扫描所有的分表来查找或更新所有符合条件的数据。因此对于因业务SQL错误而造成update、delete全表扫可能会导致严重的后果。如果希望禁止所有update、delete的全表扫可以参考 禁止全表写入的配置。

规则配置相关

1.主辅维度的使用

主维度只有一个,可读可写;辅维度可根据需求选配,只能读(主辅维度说明参考4.1)。Insert语句必须带有主维度的shardKey,辅维度的select需要带辅维度的shardKey且不能带主维度的。

2.分表但是不分库的情况如何配置?

有时业务可能只分表而不分库,且在这个库中可能还有单表。默认情况下,使用ShardDataSource访问单表也需要配置规则,实际使用起来会很麻烦,因此针对这种情况做了部分优化 不分库只分表情况下默认路由策略。

3.SQL中不带分表键怎么办?

某些特殊情况下,shardKey可能不在业务表中而是用户额外指定的值,或者需要执行的SQL中可能不会带有对应的shardKey但希望能路由到某个指定表上执行,这时可以参考API的方式指定shardKey zebra的API指南。

Clone this wiki locally