在我们的项目发展到一定阶段之后,随着数据量的增大,分库分表就变成了一件非常自然的事情。常见的分库分表方式有两种:客户端模式和服务器模式,这两种的典型代表有sharding-jdbc和MyCat。所谓的客户端模式是指在各个连接数据库的客户端中引用额外提供的jar包,以对连接数据库的过程进行封装,从而达到根据客户端的配置,将不同的请求分发到不同的数据库中的目的;而服务端模式是指,搭建一个数据库服务,这个服务只是架设在真实数据库集群前的一个代理层,其能够正常接收和解析客户端传入的SQL语句,然后根据其配置,将该SQL语句解析之后发送到各个真实的服务器执行,最终由代理层收集执行的结果并将该结果返回。服务器模式下,客户端所连接的服务完全就像是一个数据库服务,这种方式对于客户端的侵入性是非常小的。
作为服务端模式的典型代表,MyCat不仅提供了丰富的分库分表策略,也提供了非常灵活的读写分离策略,并且其对客户端的侵入性是非常小的。本文主要讲解MyCat主要提供的分库分表策略,并且还会讲解MyCat如果自定义分库分表策略。
1. 配置格式介绍
在讲解MyCat分库分表策略之前,我们首先介绍一下其配置文件的格式。在MyCat中,配置文件主要有两个:schema.xml和rule.xml。顾名思义,这两个配置文件分别指定了MyCat所代理的数据库集群的配置和分库分表的相关策略。schema.xml中的典型配置如下:
rule="mod-long"/>
dbType="mysql" dbDriver="native">
select user()
dbType="mysql" dbDriver="native">
select user()
dbType="mysql" dbDriver="native">
select user()
可以看到,schema.xml指定的是各个数据库节点与MyCat中虚拟数据库和表的关联关系,并且指定了当前表的分表策略,比如这里的mod-long。在rule.xml中则指定了具体的分表策略及其所使用的算法实现类,如下是一个典型的rule.xml的配置:
id
mod-long
3
结合schema.xml和rule.xml两个配置文件的配置,我们可以看出,MyCat首先通过schema.xml指定了当前服务器中所虚拟的数据库,以及该数据库中所对应的表的配置,比如这里的mydb和t_goods,实际上,我们在通过数据库连接工具连接到MyCat数据库时,看到的表定义都是通过该配置文件得来的,其本身并没有通过读取真实的数据库节点来获得这些配置。在指定了虚拟数据库和虚拟表之后,在schema.xml中,通过表级别的配置,又分别指定了当前表所关联的数据节点配置,以及该表是如何进行分库分表的。而具体的分库分表实现类则在rule.xml中进行了配置。另外,通过上面的配置,我们也可以看出,MyCat是不支持通过客户端连接工具来创建表的,其所有的额表必须提前在配置文件中进行定义。
2. 分库分表策略
1. 取余
关于取余的策略,这种方式上面已经进行了详细的介绍,主要的策略就是根据指定的字段对数据库节点数进行取余,从而将其插入到对应的数据库中,这里不再赘述。
2. 按照范围分片
按照范围分片,顾名思义,就是首先对整体数据进行范围划分,然后将各个范围区间分配到对应的数据库节点上,当用户插入数据时,根据指定字段的值,判断其属于哪个范围,然后将数据插入到该范围对应的数据库节点上。需要注意的是,这里会配置一个默认的范围,当用户插入的数据不再任何指定的范围内时,该数据将会被插入到默认节点上。如下是按范围分片的配置:
members
range-members-count
files/company-range-partition.txt
0
0-10=0
11-50=1
51-100=2
101-1000=0
1001-9999=1
10000-9999999=2
3. 按照日期进行分片
按照日期分片,这种方式相对来说理解稍微复杂一点,我们这里直接展示一个配置示例:
order_time
sharding-by-date
yyyy-MM-dd
-01-01
-02-02
20
上面的配置中,比较好理解的是sBeginDate和sEndDate,这两个参数指定了所有分区将会划分的总的分区时间段范围;而sPartitionDay则指定了每个分区所占用的时间段范围,比如这里sPartitionDay为20,sBeginDate为-01-01,sEndDate为-02-02,也就是说根据20天一段来分,整个时间段将会被分为-01-01~-01-21和-01-21~-02-02。关于这里划分的分区数,这里需要说明两点:
这里我们将整个时间段切割之后,只能得到两个分区,但我们的数据库节点配置了三个,此时,只有第一个节点和第二个节点将会被使用,第三个将始终不会用到;
如果我们提供的数据库节点数比切割的分区数要小的话,那么就会有一部分分区没有与其指定的数据库节点,此时就会抛出异常;
另外,我们需要着重强调的一点是,在正常使用的过程中,如果配置了结束时间,那么始终会有一天我们的时间会超出结束时间,但如果我们将结束时间配置得非常大,那么就会出现一个问题就是所需要的分区数会比我们的数据库节点数要多,此时就会抛出异常。在这一点上,MyCat允许我们的分区字段时间比结束时间要大,也就是说,插入的字段值可以是-02-02以后的日期。此时目标日期所在的分区计算方式如下:
int targetPartition = ((endTimeMills - sBeginDateMills) / (partitionDurationMills)) % nPartitons;
endTimeMills表示目标要计算的时间戳;
sBeginDateMills表示配置的开始时间戳;
partitionDurationMills表示每个分区时间段的时间戳时长,比如这里就是20 * 24 * 60 * 60 * 1000;
nPartitons表示当前在sBeginDate和sEndDate之间划分的分区数量,根据前面我们的演示知道其为2;
上面的公式,从整体上来理解,其实比较简单,本质上就是将目标时间与开始时间之间的差值除以分区长度,从而计算得出目标时间与开始时间之间的分区数,然后将该分区数与当前划分的分区数进行取模,从而得出其所在的分区。下面我们以四条数据为例,讲解其将会落入的目标数据节点:
insert into t_order(`id`, `order_time`) values (1, '-01-05'); # 分区0,db1
insert into t_order(`id`, `order_time`) values (1, '-01-25'); # 分区1,db2
insert into t_order(`id`, `order_time`) values (1, '-02-05'); # 分区1,db2
insert into t_order(`id`, `order_time`) values (1, '-02-15'); # 分区2,db1
在上述配置中,我们是配置了分区的结束时间的,实际上,在这种分区策略下,我们也可以不配置结束时间,如果不配置结束时间,那么需要注意的一点是,目标时间所在的分区计算公式如下:
int targetPartition = (endTimeMills - sBeginDateMills) / (partitionDurationMills);
相信读者朋友已经看出来了,这就是计算目标时间与开始时间中间间隔了多少个分区,然后将该值作为目标分区,也就是数据会落到目标数据库节点上,此时,随着时间的持续增长,如果数据库节点的数目比当前计算得到的分区数要小,那么就会抛出异常。
4. 按照月份进行分片
按照月份进行分片,顾名思义,就是以月为单位,判断目标时间在哪个月内,然后就将数据分配到这个月对应的数据节点上。如下是按照月份进行分片的配置示例:
create_time
partbymonth
yyyy-MM-dd
根据上面的配置,我们需要说明如下几点:
如果没有配置开始时间和结束时间,那么数据库的节点数必须大于等于12,因为一年有12个月,数据将会根据其所在的月份分配到对应的数据节点上;
如果只配置了开始时间(字段名为sBeginDate),此时需要注意两点:
如果目标时间小于开始时间,则会抛出异常,因为目标时间与开始时间之间的分区数值为负数;
如果目标时间大于开始时间,则会计算目标时间与开始时间之间的月份数,此时该月份数就会作为目标数据节点(其并不会对12取模之后存放),因而如果计算得到的数值比我们的数据库节点数要大,就会抛出异常;
如果配置了开始时间,也配置了结束时间(字段名为sEndDate),需要注意的是,此时并不会按照目标时间所在的月份而将其放到对应的数据库节点上,而是首先会计算开始时间和结束时间之间相隔的月份数nPartitions,然后计算目标时间与开始时间之间的月份数targetPartitions,然后将两者取模,即targetPartitions % nPartitions,从而得到其所在的数据库节点。另外,需要注意的是,如果目标时间小于开始时间,那么targetPartitions就是一个负数,此时其所在的分区计算公式就为nPartitions - targetPartitions % nPartitions,也就是说,目标分区会循环的从最大的分区值往下倒数。
5. 按照枚举值分片
按照枚举值分片比较适合于某个字段只有固定的几个值的情况,比如省份。通过配置文件将每个枚举值对应的数据库节点进行映射,这样对于指定类型的数据,就会被分配到同一个数据库实例中。如下是按照枚举值分片的一个示例:
province
sharding-by-province-func
class="io.mycat.route.function.PartitionByFileMap">
files/sharding-by-province.txt
0
0
1001=0
1002=1
1003=2
1004=0
6. 范围取模
范围取模分片的优点在于,既拥有范围分片的固定范围数据不做迁移的优点,也拥有了取模分片对于热点数据均匀分布的优点。首先我们还是以一个示例进行讲解:
id
rang-mod
0
files/partition-range-mod.txt
0-5=1
6-10=2
11-15=1
关于范围取模分片,这里需要着重说明一下其概念:
在最后的partition-range-mod.txt文件中,我们可以看到,其每一行在等号前指定了一个不想交的范围,这个范围表示的就是目标分区字段的值将会落在哪个范围内;
等号后面有一个数字,需要注意的是,这个数字并不是指数据库节点id,而是当前范围将会占用的数据库节点数目,比如这里的范围0-5内的数据将会被分配到1个数据库节点上,而范围6-10内的数据将会被分配到2个数据库节点上;
等号后面指定了当前范围所需要使用的分片数,而该范围的数据在这几个数据库节点的分布方式是通过取模的方式来实现的,也就是说,在大的方向上,整体数据被切分为多个范围,然后在每个范围内,数据根据取模的方式分配到不同的数据节点上。
这也就是范围取模分片的概念的由来,这种分片方式的优点在于,在进行扩容和数据迁移的时候,不相关的范围内的数据是不需要移动的。比如假设我们0-5范围内的数据非常多,1个数据库实例无法承受,此时就可以增加一个数据库实例,然后将配置改为0-5=2,接着将之前该范围内的数据库的数据导出,然后由重新导入,以平均分配到这两个数据库节点上。可以看出,这种方式扩容,对于其余两个范围内的数据库实例是没有影响的。最后,需要着重强调的一点是,既然等号后面表示所需要的数据库实例数量,那么等号后面的数字加起来的和一定要小于我们所提供的真实数据库实例的数量。
7. 二进制取模范围分片
二进制取模分片的方式与范围取模非常相似,但也有不同,其分片方式主要是根据目标分片字段的低10位的值来判断其属于哪个分片。我们首先还是以一个示例进行讲解:
id
func1
2,1
256,512
关于上面的分片方式的分片效果,其总共有1+2 = 3个分片,而每个分片中所分配的范围分别为0-255,256-511和512-1023。图示如下:
|----------------------------------1024----------------------------------|
|-------256-------|-------256--------|-----------------512---------------|
|---partition0----|----partition1----|-------------partition2------------|
8. 一致性hash分片
一致性hash分片方式上面的二进制取模方式非常相似,不过一致性hash的虚拟槽的概念更强,并且一致性hash分片的虚拟槽的数量是可配置的。如下是一个典型的一致性hash分片的配置方式:
id
murmur
0
3
160
/Users/zhangxufeng/xufeng.zhang/mycat/bucketMap.txt
9. 按照目标字段前缀指定的进行分区
按照目标字段前缀进行分片,这种方式就比较好理解,其会获取到指定分区字段的前缀值,然后将其转换为十进制数字,将其作为分区值,如果该数字超过了分区数量,则会将当前数据放在默认分区。如下是按照字符串前缀方式分区的配置示例:
id
sharding-by-substring
class="io.mycat.route.function.PartitionDirectBySubString">
0
2
3
0
10. 按照前缀ASCII码和值进行取模范围分片
按照前缀ASCII码和值进行取模,顾名思义,就是取了前缀之后,将其转换为ASCII码值,然后对取模基数进行取模,最后将求得的余数按照配置文件中的范围分配到对应的数据库节点上。如下是该分区方式的配置示例:
id
sharding-by-prefixpattern
class="io.mycat.route.function.PartitionByPrefixPattern">
256
5
files/partition-pattern.txt
0-100=0
101-200=1
201-256=2
这里的范围对应关系需要注意的是,其需要将我们设置的取模基数patternValue的整个范围都进行覆盖,否则对于没有覆盖的数据将会报错。
3. 小结
本文首先对MyCat进行了简要介绍,并且讲解了其配置文件的配置方式。然后着重介绍了MyCatt提供的十种分区策略。