作为服务端模式的典型代表,MyCat不仅提供了丰富的分库分表策略,也提供了非常灵活的读写分离策略,并且其对客户端的侵入性是非常小的。本文主要讲解MyCat主要提供的分库分表策略,并且还会讲解MyCat如果自定义分库分表策略。
1. 配置格式介绍
在讲解MyCat分库分表策略之前,我们首先介绍一下其配置文件的格式。在MyCat中,配置文件主要有两个:schema.xml和rule.xml。顾名思义,这两个配置文件分别指定了MyCat所代理的数据库集群的配置和分库分表的相关策略。schema.xml中的典型配置如下:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><?xml version="1.0"?>
- <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
- <mycat:schema xmlns:mycat="http://io.mycat/">
- <!-- 指定了对外所展示的数据库名称,也就是说,客户端连接MyCat数据库时,制定的database为mydb
- 而当前数据库中的表的配置就是根据下面的配置而来的 -->
- <schema name="mydb" checkSQLschema="true" sqlMaxLimit="100">
- <!-- 定义了一个t_goods表,该表的主键是id,该字段是自增长的,并且该表的数据会被分配到dn1,dn2和
- dn3上,这三个指的是当前MyCat数据库所代理的真实数据库的节点名,每个节点的具体配置在下面的
- 配置中。这里rule属性指定了t_goods表中的数据分配到dn1,dn2和dn3上的策略,mod-long指的是
- 按照长整型取余的方式分配,也就是按照id对节点数目进行取余 -->
- <table name="t_goods" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3"
- rule="mod-long"/>
- </schema>
- <!-- 分别指定了dn1,dn2和dn3三个节点与对应的数据库的关系,dataHost对应的就是下面的数据库节点配置 -->
- <dataNode name="dn1" dataHost="dhost1" database="db1"/>
- <dataNode name="dn2" dataHost="dhost2" database="db2"/>
- <dataNode name="dn3" dataHost="dhost3" database="db3"/>
- <!-- 这里分别指定了各个数据库节点的配置 -->
- <dataHost name="dhost1" maxCon="1000" minCon="10" balance="0" writeType="0"
- dbType="mysql" dbDriver="native">
- <heartbeat>select user()</heartbeat>
- <writeHost host="hostM1" url="localhost:3306" user="root" password="password"/>
- </dataHost>
- <dataHost name="dhost2" maxCon="1000" minCon="10" balance="0" writeType="0"
- dbType="mysql" dbDriver="native">
- <heartbeat>select user()</heartbeat>
- <writeHost host="hostM2" url="localhost:3306" user="root" password="password"/>
- </dataHost>
- <dataHost name="dhost3" maxCon="1000" minCon="10" balance="0" writeType="0"
- dbType="mysql" dbDriver="native">
- <heartbeat>select user()</heartbeat>
- <writeHost host="hostM3" url="localhost:3306" user="root" password="password"/>
- </dataHost>
- </mycat:schema>
可以看到,schema.xml指定的是各个数据库节点与MyCat中虚拟数据库和表的关联关系,并且指定了当前表的分表策略,比如这里的mod-long。在rule.xml中则指定了具体的分表策略及其所使用的算法实现类,如下是一个典型的rule.xml的配置:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;">"1.0" encoding="UTF-8"?>
- mycat:rule SYSTEM "rule.dtd">
- <mycat:rule xmlns:mycat="http://io.mycat/">
-
- <tableRule name="mod-long">
- <rule>
- <columns>idcolumns>
- <algorithm>mod-longalgorithm>
- rule>
- tableRule>
-
-
- <function name="mod-long" class="io.mycat.route.function.PartitionByMod">
-
- <property name="count">3property>
- function>
- mycat:rule>
结合schema.xml和rule.xml两个配置文件的配置,我们可以看出,MyCat首先通过schema.xml指定了当前服务器中所虚拟的数据库,以及该数据库中所对应的表的配置,比如这里的mydb和t_goods,实际上,我们在通过数据库连接工具连接到MyCat数据库时,看到的表定义都是通过该配置文件得来的,其本身并没有通过读取真实的数据库节点来获得这些配置。在指定了虚拟数据库和虚拟表之后,在schema.xml中,通过表级别的配置,又分别指定了当前表所关联的数据节点配置,以及该表是如何进行分库分表的。而具体的分库分表实现类则在rule.xml中进行了配置。另外,通过上面的配置,我们也可以看出,MyCat是不支持通过客户端连接工具来创建表的,其所有的额表必须提前在配置文件中进行定义。
2. 分库分表策略
1. 取余
关于取余的策略,这种方式上面已经进行了详细的介绍,主要的策略就是根据指定的字段对数据库节点数进行取余,从而将其插入到对应的数据库中,这里不再赘述。
2. 按照范围分片
按照范围分片,顾名思义,就是首先对整体数据进行范围划分,然后将各个范围区间分配到对应的数据库节点上,当用户插入数据时,根据指定字段的值,判断其属于哪个范围,然后将数据插入到该范围对应的数据库节点上。需要注意的是,这里会配置一个默认的范围,当用户插入的数据不再任何指定的范围内时,该数据将会被插入到默认节点上。如下是按范围分片的配置:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_company" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="range-sharding-by-members-count"/>
- <!-- rule.xml -->
- <tableRule name="range-sharding-by-members-count">
- <rule>
- <!-- 指定了分片字段 -->
- <columns>members</columns>
- <algorithm>range-members-count</algorithm>
- </rule>
- </tableRule>
- <function name="range-members-count" class="io.mycat.route.function.AutoPartitionByLong">
- <!-- 指定了范围分片的”范围-节点“的对应策略 -->
- <property name="mapFile">files/company-range-partition.txt</property>
- <!-- 指定了超出范围的数据将会被分配的数据节点 -->
- <property name="defaultNode">0</property>
- </function>
- <!-- 上面mapFile属性指定的company-range-partition.txt文件内容,这里指定了具体的范围与数据节点的对应关系
- -->
- 0-10=0
- 11-50=1
- 51-100=2
- 101-1000=0
- 1001-9999=1
- 10000-9999999=2
3. 按照日期进行分片
按照日期分片,这种方式相对来说理解稍微复杂一点,我们这里直接展示一个配置示例:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_order" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="order-sharding-by-date"/>
- <!-- rule.xml -->
- <tableRule name="order-sharding-by-date">
- <rule>
- <!-- 指定分区字段为order_time -->
- <columns>order_time</columns>
- <!-- 指定分区算法为sharding-by-date -->
- <algorithm>sharding-by-date</algorithm>
- </rule>
- </tableRule>
- <!-- 指定分区算法使用的实现类是io.mycat.route.function.PartitionByDate,这里需要传如四个属性:
- dateFormat表示下面sBeginDate、sEndDate以及分区字段的数据值所使用的日期格式化方式;
- sBeginDate指定了分区范围的开始时间;
- sEndDate指定了分区范围的结束时间;
- sPartitionDay指定了每个分区间隔的时间范围长度-->
- <function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate">
- <property name="dateFormat">yyyy-MM-dd</property>
- <property name="sBeginDate">2019-01-01</property>
- <property name="sEndDate">2019-02-02</property>
- <property name="sPartionDay">20</property>
- </function>
上面的配置中,比较好理解的是sBeginDate和sEndDate,这两个参数指定了所有分区将会划分的总的分区时间段范围;而sPartitionDay则指定了每个分区所占用的时间段范围,比如这里sPartitionDay为20,sBeginDate为2019-01-01,sEndDate为2019-02-02,也就是说根据20天一段来分,整个时间段将会被分为2019-01-01~2019-01-21和2019-01-21-2019-02-02。关于这里划分的分区数,这里需要说明两点:
另外,我们需要着重强调的一点是,在正常使用的过程中,如果配置了结束时间,那么始终会有一天我们的时间会超出结束时间,但如果我们将结束时间配置得非常大,那么就会出现一个问题就是所需要的分区数会比我们的数据库节点数要多,此时就会抛出异常。在这一点上,MyCat允许我们的分区字段时间比结束时间要大,也就是说,插入的字段值可以是2019-02-02以后的日期。此时目标日期所在的分区计算方式如下:
<pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;">int targetPartition = ((endTimeMills - sBeginDateMills) / (partitionDurationMills)) % nPartitons;
上面的公式,从整体上来理解,其实比较简单,本质上就是将目标时间与开始时间之间的差值除以分区长度,从而计算得出目标时间与开始时间之间的分区数,然后将该分区数与当前划分的分区数进行取模,从而得出其所在的分区。下面我们以四条数据为例,讲解其将会落入的目标数据节点:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;">insert into t_order(`id`, `order_time`) values (1, '2019-01-05'); # 分区0,db1
- insert into t_order(`id`, `order_time`) values (1, '2019-01-25'); # 分区1,db2
- insert into t_order(`id`, `order_time`) values (1, '2019-02-05'); # 分区1,db2
- insert into t_order(`id`, `order_time`) values (1, '2019-02-15'); # 分区2,db1
在上述配置中,我们是配置了分区的结束时间的,实际上,在这种分区策略下,我们也可以不配置结束时间,如果不配置结束时间,那么需要注意的一点是,目标时间所在的分区计算公式如下:
<pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;">int targetPartition = (endTimeMills - sBeginDateMills) / (partitionDurationMills);
相信你们已经看出来了,这就是计算目标时间与开始时间中间间隔了多少个分区,然后将该值作为目标分区,也就是数据会落到目标数据库节点上,此时,随着时间的持续增长,如果数据库节点的数目比当前计算得到的分区数要小,那么就会抛出异常。
4. 按照月份进行分片
按照月份进行分片,顾名思义,就是以月为单位,判断目标时间在哪个月内,然后就将数据分配到这个月对应的数据节点上。如下是按照月份进行分片的配置示例:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_bank" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3,dn4,dn5,dn6,dn7,dn8,dn9,dn10,dn11,dn12" rule="sharding-by-month"/>
- <!-- rule.xml -->
- <tableRule name="sharding-by-month">
- <rule>
- <columns>create_time</columns>
- <algorithm>partbymonth</algorithm>
- </rule>
- </tableRule>
- <function name="partbymonth" class="io.mycat.route.function.PartitionByMonth">
- <property name="dateFormat">yyyy-MM-dd</property>
- </function>
根据上面的配置,我们需要说明如下几点:
5. 按照枚举值分片
按照枚举值分片比较适合于某个字段只有固定的几个值的情况,比如省份。通过配置文件将每个枚举值对应的数据库节点进行映射,这样对于指定类型的数据,就会被分配到同一个数据库实例中。如下是按照枚举值分片的一个示例:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_customer" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="sharding-by-province"/>
- <tableRule name="sharding-by-province">
- <rule>
- <!-- 指定了分区字段 -->
- <columns>province</columns>
- <!-- 指定分区算法 -->
- <algorithm>sharding-by-province-func</algorithm>
- </rule>
- </tableRule>
- <!-- 按照枚举值分片的算法,这里mapFile中保存了分区字段中各个枚举值与目标数据库实例的对应关系;
- type字段表示了分区字段的key的类型,0表示数字,非0表示字符串;defaultNode指定了没有配置映射
- 关系的数据其存储的数据库节点 -->
- <function name="sharding-by-province-func"
- class="io.mycat.route.function.PartitionByFileMap">
- <property name="mapFile">files/sharding-by-province.txt</property>
- <property name="type">0</property>
- <property name="defaultNode">0</property>
- </function>
- <!-- sharding-by-province.txt -->
- 1001=0
- 1002=1
- 1003=2
- 1004=0
6. 范围取模
范围取模分片的优点在于,既拥有范围分片的固定范围数据不做迁移的优点,也拥有了取模分片对于热点数据均匀分布的优点。首先我们还是以一个示例进行讲解:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_car" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="auto-sharding-rang-mod"/>
- <!-- rule.xml -->
- <tableRule name="auto-sharding-rang-mod">
- <rule>
- <!-- 指定id字段为范围取模分片的字段 -->
- <columns>id</columns>
- <algorithm>rang-mod</algorithm>
- </rule>
- </tableRule>
- <!-- 这里defaultNode指的是,如果目标字段的值不在范围内,则将其放置到默认节点上;mapFile指定了
- 范围与分片数的一个对应关系 -->
- <function name="rang-mod" class="io.mycat.route.function.PartitionByRangeMod">
- <property name="defaultNode">0</property>
- <property name="mapFile">files/partition-range-mod.txt</property>
- </function>
- <!-- partition-range-mod.txt -->
- 0-5=1
- 6-10=2
- 11-15=1
关于范围取模分片,这里需要着重说明一下其概念:
这也就是范围取模分片的概念的由来,这种分片方式的优点在于,在进行扩容和数据迁移的时候,不相关的范围内的数据是不需要移动的。比如假设我们0-5范围内的数据非常多,1个数据库实例无法承受,此时就可以增加一个数据库实例,然后将配置改为0-5=2,接着将之前该范围内的数据库的数据导出,然后由重新导入,以平均分配到这两个数据库节点上。可以看出,这种方式扩容,对于其余两个范围内的数据库实例是没有影响的。最后,需要着重强调的一点是,既然等号后面表示所需要的数据库实例数量,那么等号后面的数字加起来的和一定要小于我们所提供的真实数据库实例的数量。
7. 二进制取模范围分片
二进制取模分片的方式与范围取模非常相似,但也有不同,其分片方式主要是根据目标分片字段的低10位的值来判断其属于哪个分片。我们首先还是以一个示例进行讲解:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_bike" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="rule1"/>
- <!-- rule.xml -->
- <tableRule name="rule1">
- <rule>
- <!-- 指定了分片字段 -->
- <columns>id</columns>
- <algorithm>func1</algorithm>
- </rule>
- </tableRule>
- <!-- 这里分片算法中需要传如两个参数:partitionCount和partitionLength。这两个字段的含义分别为分区数量
- 和分区长度,但是需要注意的是,这里的分区数量和分区长度相乘之后加起来必须为1024,比如这里的
- 2 * 256 + 1 * 512 = 1024。至于为什么必须为1024,这主要是因为二进制取模分片是取目标分区字段的
- 低10位数据作为其所在的槽,由于低10位最大为1024,因而这里配置的加和必须为1024。 -->
- <function name="func1" class="io.mycat.route.function.PartitionByLong">
- <property name="partitionCount">2,1</property>
- <property name="partitionLength">256,512</property>
- </function>
关于上面的分片方式的分片效果,其总共有1+2 = 3个分片,而每个分片中所分配的范围分别为0-255,256-511和512-1023。图示如下:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;">|----------------------------------1024----------------------------------|
- |-------256-------|-------256--------|-----------------512---------------|
- |---partition0----|----partition1----|-------------partition2------------|
8. 一致性hash分片
一致性hash分片方式上面的二进制取模方式非常相似,不过一致性hash的虚拟槽的概念更强,并且一致性hash分片的虚拟槽的数量是可配置的。如下是一个典型的一致性hash分片的配置方式:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_house" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="sharding-by-murmur"/>
- <!-- rule.xml -->
- <tableRule name="sharding-by-murmur">
- <rule>
- <columns>id</columns>
- <algorithm>murmur</algorithm>
- </rule>
- </tableRule>
- <!-- 下面的属性中,count指定了要分片的数据库节点数量,必须指定,否则没法分片;virtualBucketTimes指的是
- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍;
- weightMapFile指定了节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,
- 以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,
- 否则以1代替;bucketMapPath用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,
- 会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,
- 就不会输出任何东西-->
- <function name="murmur" class="io.mycat.route.function.PartitionByMurmurHash">
- <property name="seed">0</property><!-- 默认是0 -->
- <property name="count">3</property>
- <property name="virtualBucketTimes">160</property><!-- -->
- <!-- <property name="weightMapFile">weightMapFile</property> -->
- <property name="bucketMapPath">
- /Users/zhangxufeng/xufeng.zhang/mycat/bucketMap.txt</property>
- </function>
9. 按照目标字段前缀指定的进行分区
按照目标字段前缀进行分片,这种方式就比较好理解,其会获取到指定分区字段的前缀值,然后将其转换为十进制数字,将其作为分区值,如果该数字超过了分区数量,则会将当前数据放在默认分区。如下是按照字符串前缀方式分区的配置示例:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_community" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="sharding-by-substring"/>
- <!-- rule.xml -->
- <tableRule name="sharding-by-substring">
- <rule>
- <!-- 指定按照id字段进行分区 -->
- <columns>id</columns>
- <algorithm>sharding-by-substring</algorithm>
- </rule>
- </tableRule>
- <function name="sharding-by-substring"
- class="io.mycat.route.function.PartitionDirectBySubString">
- <!-- 指定前缀的开始位置 -->
- <property name="startIndex">0</property>
- <!-- 指定前缀的大小 -->
- <property name="size">2</property>
- <!-- 指定了分区数量 -->
- <property name="partitionCount">3</property>
- <!-- 指定了默认分区 -->
- <property name="defaultPartition">0</property>
- </function>
10. 按照前缀ASCII码和值进行取模范围分片
按照前缀ASCII码和值进行取模,顾名思义,就是取了前缀之后,将其转换为ASCII码值,然后对取模基数进行取模,最后将求得的余数按照配置文件中的范围分配到对应的数据库节点上。如下是该分区方式的配置示例:
- <pre style="box-sizing: border-box; font-family: monospace; font-size: 1em; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all;"><!-- schema.xml -->
- <table name="t_phone" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="sharding-by-prefixpattern"/>
- <!-- rule.xml -->
- <tableRule name="sharding-by-prefixpattern">
- <rule>
- <!-- 指定id为分区字段 -->
- <columns>id</columns>
- <algorithm>sharding-by-prefixpattern</algorithm>
- </rule>
- </tableRule>
- <!-- 这里的patternValue指定的是取模基数;prefixLength表示对指定字段的前多少位进行截取计算ASCII码;
- mapFile中指定了取模的余数范围与目标数据库节点的对应关系 -->
- <function name="sharding-by-prefixpattern"
- class="io.mycat.route.function.PartitionByPrefixPattern">
- <property name="patternValue">256</property>
- <property name="prefixLength">5</property>
- <property name="mapFile">files/partition-pattern.txt</property>
- </function>
- <!-- partition-pattern.txt -->
- 0-100=0
- 101-200=1
- 201-256=2
这里的范围对应关系需要注意的是,其需要将我们设置的取模基数patternValue的整个范围都进行覆盖,否则对于没有覆盖的数据将会报错。
完结:
本文首先对MyCat进行了简要介绍,并且讲解了其配置文件的配置方式。然后着重介绍了MyCatt提供的十种分区策略。