分库分表解决方案 MyCat 系列 数据分片篇

1,765 阅读21分钟

白菜Java自习室 涵盖核心知识

分库分表解决方案 MyCat 系列 数据切分篇
分库分表解决方案 MyCat 系列 基本概念篇
分库分表解决方案 MyCat 系列 数据分片篇
分库分表解决方案 MyCat 系列 集群事务篇
分库分表解决方案 MyCat 系列 路由分发篇

1. Mycat 的分片 join

1.1. join 概述

Join 绝对是关系型数据库中最常用一个特性,然而在分布式环境中,跨分片的 join 确是最复杂的,最难解决一个问题。 下面我们简单介绍下各种 Join 操作。

Inner join

内连接,也叫等值连接,inner join 产生同时符合 A 表和 B 表的一组数据。如图:

INNER JOIN.jpg

Left join

左连接从 A 表(左)产生一套完整的记录,与匹配的 B 表记录(右表)。如果没有匹配,右侧将包含 null,在 Mysql 中等同于 left outer join。 如图:

LEFT JOIN.jpg

Right join

同 Left join,AB 表互换即可。

Cross join

交叉连接,得到的结果是两个表的乘积,即笛卡尔积。笛卡尔(Descartes)乘积又叫直积。假设集合 A={a,b},集合 B={0,1,2},则 两个集合的笛卡尔积为{(a,0),(a,1),(a,2),(b,0),(b,1), (b,2)}。可以扩展到多个集合的情况。类似的例子有,如果 A 表示某学校学生的集合,B 表示该学校所有课程的集合,则 A 与 B 的笛卡尔积表示所有可能的选课情况。

Full join

全连接产生的所有记录(双方匹配记录)在表 A 和表 B。如果没有匹配,则对面将包含 null。

FULL JOIN.jpg

性能建议

  • 尽量避免使用 Left join 或 Right join,而用 Inner join
  • 在使用 Left join 或 Right join 时,ON 会优先执行,where 条件在最后执行,所以在使用过程中,条件尽可能的在 ON 语句中判断,减少 where 的执行
  • 少用子查询,而用 join。

Mycat 目前版本支持跨分片的 join,主要实现的方式有四种。全局表,ER 分片,catletT(人工智能)和 ShareJoin,ShareJoin 在开发版中支持,前面三种方式 1.3.0.1 支持。

1.2. Mycat 的全局表

一个真实的业务系统中,往往存在大量的类似字典表的表格,它们与业务表之间可能有关系,这种关系,可 以理解为“标签”,而不应理解为通常的“主从关系”,这些表基本上很少变动,可以根据主键 ID 进行缓存,下面这张图说明了一个典型的“标签关系”图:

标签关系图.jpg

在分片的情况下,当业务表因为规模而进行分片以后,业务表与这些附属的字典表之间的关联,就成了比较棘手的问题,考虑到字典表具有以下几个特性:

  • 变动不频繁
  • 数据量总体变化不大
  • 数据规模不大,很少有超过数十万条记录

鉴于此,MyCAT 定义了一种特殊的表,称之为“全局表”,全局表具有以下特性:

  • 全局表的插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性
  • 全局表的查询操作,只从一个节点获取
  • 全局表可以跟任何一个表进行 JOIN 操作

将字典表或者符合字典表特性的一些表定义为全局表,则从另外一个方面,很好的解决了数据 JOIN 的难题。 通过全局表+基于 E-R 关系的分片策略,MyCAT 可以满足 80%以上的企业应用开发。

配置

全局表配置比较简单,不用写 Rule 规则,如下配置即可:

<table name="company" primaryKey="ID" type="global" dataNode="dn1,dn2,dn3" /> 

需要注意的是,全局表每个分片节点上都要有运行创建表的 DDL 语句。

1.3. ER Join

MyCAT 借鉴了 NewSQL 领域的新秀 Foundation DB 的设计思路,Foundation DB 创新性的提出了 Table Group 的概念,其将子表的存储位置依赖于主表,并且物理上紧邻存放,因此彻底解决了 JION 的效率和性能问题,根据这一思路,提出了基于 E-R 关系的数据分片策略,子表的记录与所关联的父表记录存放在同一个数据分片上。

customer 采用 sharding-by-intfile 这个分片策略,分片在 dn1,dn2 上,orders 依赖父表进行分片,两个表的关联关系为 orders.customer_id=customer.id。于是数据分片和存储的示意图如下:

子表与主表同一分片.jpg

这样一来,分片 Dn1 上的的 customer 与 Dn1 上的 orders 就可以进行局部的 JOIN 联合,Dn2 上也如此,再合并两个节点的数据即可完成整体的 JOIN,试想一下,每个分片上 orders 表有 100 万条,则 10 个分片就有 1 个亿,基于 E-R 映射的数据分片模式,基本上解决了 80%以上的企业应用所面临的问题。

配置

以上述例子为例,schema.xml 中定义如下的分片配置:

<table name="customer" dataNode="dn1,dn2" rule="sharding-by-intfile"> 
    <childTable name="orders" joinKey="customer_id" parentKey="id"/> 
</table> 

1.4. Share join

ShareJoin 是一个简单的跨分片 Join,基于 HBT 的方式实现。目前支持 2 个表的 join,原理就是解析 SQL 语句,拆分成单表的 SQL 语句执行,然后把各个节点的数据汇集。

配置

支持任意配置的 A,B 表如:

A,B 的 dataNode 相同

<table name="A" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" /> 
<table name="B" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />

A,B 的 dataNode 不同

<table name="A" dataNode="dn1,dn2 " rule="auto-sharding-long" /> 
<table name="B" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />

<table name="A" dataNode="dn1 " rule="auto-sharding-long" /> 
<table name="B" dataNode=" dn2,dn3" rule="auto-sharding-long" /> 

1.5. catlet(人工智能)

解决跨分片的 SQL JOIN 的问题,远比想象的复杂,而且往往无法实现高效的处理,既然如此,就依靠人工的智力,去编程解决业务系统中特定几个必须跨分片的 SQL 的 JOIN 逻辑,MyCAT 提供特定的 API 供程序员调用,这就是 MyCAT 创新性的思路——人工智能。以一个跨节点的 SQL 为例。

select a.id,a.name,b.title from a,b where a.id=b.id;

其中 a 在分片 1,2,3 上,b 在 4,5,6 上,需要把数据全部拉到本地(MyCAT 服务器),执行 JOIN 逻辑, 具体过程如下(只是一种可能的执行逻辑):

    // 包含 MyCat.SQLEngine 
    EngineCtx ctx = new EngineCtx(); 
    String sql = "select a.id, a.name from a";
    // 在 a 表所在的所有分片上顺序执行下面的本地 SQL 
    ctx.executeNativeSQLSequnceJob(allAnodes, new DirectDBJoinHandler());

DirectDBJoinHandler 类是一个回调类,负责处理 SQL 执行过程中返回的数据包,这里的这个类,主要目的是用 a 表返回的 ID 信息,去 b 表上查询对于的记录,做实时的关联:

    public class DirectDBJoinHandler {

        // key 为 id,value 为一行记录的 Column 原始 Byte 数组,
        // 这里是 a.id,a.name,b.title 这三个要输出的字段
        private HashMap<byte[], byte[]> rows;

        public Boolean onHeader(byte[] header) {
            // 保存 Header 信息,用于从 Row 中获取 Field 字段值 
        }

        public Boolean onRowData(byte[] rowData) {
            String id = getColumnAsString("id");
            // 放入结果集,b.title 字段未知,所以先空着 
            rows.put(getColumnRawBytes("id"), rowData);
            // 满 1000 条,发送一个查询请求 
            String sql = "select b.id, b.name from b where id in(......)";
            // 此 SQL 在 B 的所有节点上并发执行,返回的结果直接输出到客户端 
            ctx.executeNativeSQLParallJob(allBNodes, sql, new MyRowOutPutDataHandler(rows));
        }

        public Boolean onRowFinished() {
        }

        public void onJobFinished() {
            if (ctx.allJobFinished()) {

            }
        }

    }

最后,增加一个 Job 事件监听器,这里是所有 Job 完成后,往客户端发送 RowEnd 包,结束整个流程。

    ctx.setJobEventListener(new JobEventHandler() {
        public void onJobFinished() {
            client.writeRowEndPackage()
        }
    });

以上提供一个 SQL 执行框架,完全是异步的模式执行,并且以后会提供更多高质量的 API,简化分布式数据处理,比如内存结合文件的数据 JOIN 算法,分组算法,排序算法等等。

1.6. Spark/Storm 对 join 扩展

看到这个标题,可能会感到很奇怪,Spark 和 Storm 和 Join 有关系吗? 有必要用 Spark,Storm 吗?

Mycat 后续的功能会引入 Spark 和 Storm 来做跨分片的 join, 大致流程是这样的在 Mycat 调用 Spark, Storm 的 API, 把数据传送到 Spark,Storm,在 Spark,Storm 进行 join,在把数据传回 Mycat,Mycat 在返回给客户端。

SparkStorm对join的扩展.jpg

2. Mycat 的分片规则

在数据切分处理中,特别是水平切分中,中间件最终要的两个处理过程就是数据的切分、数据的聚合。选择合适的切分规则,至关重要,因为它决定了后续数据聚合的难易程度,甚至可以避免跨库的数据聚合处理。

前面讲了数据切分中重要的几条原则,其中有几条是数据冗余,表分组(Table Group),这都是业务上规避跨库 join 的很好的方式,但不是所有的业务场景都适合这样的规则,因此本章将讲述如何选择合适的切分规则。

2.1. 全局表

如果你的业务中有些数据类似于数据字典,比如配置文件的配置,常用业务的配置或者数据量不大很少变动 的表,这些表往往不是特别大,而且大部分的业务场景都会用到,那么这种表适合于 Mycat 全局表,无须对数据进行切分,只要在所有的分片上保存一份数据即可,Mycat 在 Join 操作中,业务表与全局表进行 Join 聚合会优先选择相同分片内的全局表 Join,避免跨库 Join,在进行数据插入操作时,mycat 将把数据分发到全局表对应的所有分片执行,在进行数据读取时候将会随机获取一个节点读取数据。

全局表的配置如下:

<table name="t_area" primaryKey="id" type="global" dataNode="dn1,dn2" />

2.2. ER 分片表

有一类业务,例如订单(order)跟订单明细(order_detail),明细表会依赖于订单,也就是说会存在表的主从关系,这类似业务的切分可以抽象出合适的切分规则,比如根据用户 ID 切分,其他相关的表都依赖于用户 ID,再或者根据订单 ID 切分,总之部分业务总会可以抽象出父子关系的表。这类表适用于 ER 分片表,子表的记录与所关联的父表记录存放在同一个数据分片上,避免数据 Join 跨库操作。

以 order 与 order_detail 例子为例,schema.xml 中定义如下的分片配置,order,order_detail 根据 order_id 进行数据切分,保证相同 order_id 的数据分到同一个分片上,在进行数据插入操作时,Mycat 会获取 order 所在的分片,然后将 order_detail 也插入到 order 所在的分片。

<table name="order" dataNode="dn$1-32" rule="mod-long"> 
    <childTable name="order_detail" primaryKey="id" joinKey="order_id" parentKey="order_id" /> 
</table> 

2.3. 多对多关联

有一类业务场景是 “主表 A+关系表+主表 B”,举例来说就是 会员+订单+商户,对应这类业务,如何切分?

从会员的角度,如果需要查询会员购买的订单,那按照会员进行切分即可,但是如果要查询商户当天售出的订单,那又需要按照商户做切分,可是如果既要按照会员又要按照商户切分,几乎是无法实现,这类业务如何选择切分规则非常难。目前还暂时无法很好支持这种模式下的 3 个表之间的关联。目前总的原则是需要从业务角度来看,关系表更偏向哪个表,即“A 的关系”还是“B 的关系”,来决定关系表跟从那个方向存储,未来 Mycat 版本中将考虑将中间表进行双向复制,以实现从 A-关系表 以及 B-关系表的双向关联查询如下图所示:

关系表.jpg

主键分片 vs 非主键分片

当你没人任何字段可以作为分片字段的时候,主键分片就是唯一选择,其优点是按照主键的查询最快,当采用自动增长的序列号作为主键时,还能比较均匀的将数据分片在不同的节点上。若有某个合适的业务字段比较合适作为分片字段,则建议采用此业务字段分片,选择分片字段的条件如下:

  • 尽可能的比较均匀分布数据到各个节点上;
  • 该业务字段是最频繁的或者最重要的查询条件。

常见的除了主键之外的其他可能分片字段有“订单创建时间”、“店铺类别”或“所在省”等。当你找到某个合适的业务字段作为分片字段以后,不必纠结于“牺牲了按主键查询记录的性能”,因为在这种情况下, MyCAT 提供了“主键到分片”的内存缓存机制,热点数据按照主键查询,丝毫不损失性能

<table name="t_user" primaryKey="user_id" dataNode="dn$1-32" rule="mod-long"> 
    <childTable name="t_user_detail" primaryKey="id" joinKey="user_id" parentKey="user_id" /> 
</table> 

对于非主键分片的 table,填写属性 primaryKey,此时 MyCAT 会将你根据主键查询的 SQL 语句的第一次执 行结果进行分析,确定该 Table 的某个主键在什么分片上,并进行主键到分片 ID 的缓存。第二次或后续查询 mycat 会优先从缓存中查询是否有 id–>node 即主键到分片的映射,如果有直接查询,通过此种方法提高了非主键分片的查询性能。

2.4. Mycat 常用的分片规则

2.4.1. 分片枚举

通过在配置文件中配置可能的枚举 id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,而全国省份区县固定的,这类业务使用本条规则,配置如下:

<tableRule name="sharding-by-intfile"> 
<rule> 
<columns>user_id</columns> 
<algorithm>hash-int</algorithm> 
</rule> 
</tableRule> 

<function name="hash-int" class="io.mycat.route.function.PartitionByFileMap"> 
<property name="mapFile">partition-hash-int.txt</property> 
<property name="type">0</property> 
<property name="defaultNode">0</property> 
</function> 

partition-hash-int.txt 配置:

10000=0
10010=1 
DEFAULT_NODE=1 

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数,其中分片函数配置中,mapFile 标识配置文件名称,type 默认值为 0,0 表示 Integer,非零表示 String,所有的节点配置都是从 0 开始,及 0 代表节点 1。

  • defaultNode 默认节点:小于 0 表示不设置默认节点,大于等于 0 表示设置默认节点
  • 默认节点的作用:枚举分片时,如果碰到不识别的枚举值,就让它路由到默认节点
  • 如果不配置默认节点(defaultNode 值小于 0 表示不配置默认节点),碰到不识别的枚举值就会报错

2.4.2. 固定分片 hash 算法

本条规则类似于十进制的求模运算,区别在于是二进制的操作,是取 id 的二进制低 10 位,即 id 二进制 &1111111111。 此算法的优点在于如果按照 10 进制取模运算,在连续插入 1-10 时候 1-10 会被分到 1-10 个分片,增 大了插入的事务控制难度,而此算法根据二进制则可能会分到连续的分片,减少插入事务事务控制难度。

<tableRule name="rule1"> 
<rule> 
<columns>user_id</columns> 
<algorithm>func1</algorithm> 
</rule> 
</tableRule> 
  
<function name="func1" class="io.mycat.route.function.PartitionByLong"> 
<property name="partitionCount">2,1</property> 
<property name="partitionLength">256,512</property> 
</function> 

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数,partitionCount 分片个数列表,partitionLength 分片范围列表分区长度:默认为最大 2n=10242^n=1024 ,即最大支持 1024 分区约束。

注意:count, length 两个数组的长度必须是一致的。 1024=sum((count[i]length[i]))1024 = sum((count[i]*length[i])). count 和 length 两个向量的点积恒等于 1024。

本例的分区策略:希望将数据水平分成 3 份,前两份各占 25%,第三份占 50%。(故本例非均匀分区)

// |<———————1024———————————>| 
// |<—-256—>|<—-256—>|<———-512————->| 
// | partition0 | partition1 | partition2 | 
// | 共 2 份,故 count[0]=2 | 共 1 份,故 count[1]=1 |

int[] count = new int[] { 2, 1 }; 
int[] length = new int[] { 256, 512 }; 
PartitionUtil pu = new PartitionUtil(count, length);

如果需要平均分配设置:平均分为 4 分片,partitionCountpartitionLength=1024partitionCount*partitionLength=1024

<tableRule name="rule1"> 
<rule> 
<columns>user_id</columns> 
<algorithm>func1</algorithm> 
</rule> 
</tableRule> 

<function name="func1" class="io.mycat.route.function.PartitionByLong">
<property name="partitionCount">4</property> 
<property name="partitionLength">256</property> 
</function>  

2.4.3. 范围约定

此分片适用于,提前规划好分片字段某个范围属于哪个分片,

  • start<=range<=endstart <= range <= end
  • range:startendrange: start-end
  • datanode:indexdatanode: index (K=1000,M=10000)
<tableRule name="auto-sharding-long"> 
<rule> 
<columns>user_id</columns> 
<algorithm>rang-long</algorithm> 
</rule> 
</tableRule> 

<function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong"> 
<property name="mapFile">autopartition-long.txt</property> 
<property name="defaultNode">0</property> 
</function>  

配置说明

面 columns 标识将要分片的表字段,algorithm 分片函数, rang-long 函数中 mapFile 代表配置文件路径 defaultNode 超过范围后的默认节点。

所有的节点配置都是从 0 开始,及 0 代表节点 1,此配置非常简单,即预先制定可能的 id 范围到某个分片: (0-500M=0 500M-1000M=1 1000M-1500M=2) 或 (0-10000000=0 10000001-20000000=1)。

2.4.4. 字段取模

此规则为对分片字段求摸运算。

<tableRule name="mod-long"> 
<rule> 
<columns>user_id</columns> 
<algorithm>mod-long</algorithm> 
</rule> 
</tableRule> 

<function name="mod-long" class="io.mycat.route.function.PartitionByMod"> 
<!-- how many data nodes --> 
<property name="count">3</property> 
</function>  

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数, 此种配置非常明确即根据 id 进行十进制求模预算,相比固定分片 hash,此种在批量插入时可能存在批量插入单事务插入多数据分片,增大事务一致性难度。

2.4.5. 按日期(天)分片

此规则为按天分片。

<tableRule name="sharding-by-date"> 
<rule> 
<columns>create_time</columns> 
<algorithm>sharding-by-date</algorithm> 
</rule> 
</tableRule> 

<function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate"> 
<property name="dateFormat">yyyy-MM-dd</property> 
<property name="sBeginDate">2014-01-01</property> 
<property name="sEndDate">2014-01-02</property> 
<property name="sPartionDay">10</property> 
</function>  

配置说明

  • columns :标识将要分片的表字段
  • algorithm :分片函数
  • dateFormat :日期格式
  • sBeginDate :开始日期
  • sEndDate:结束日期
  • sPartionDay :分区天数,即默认从开始日期算起,分隔 10 天一个分区如果配置了 sEndDate 则代表数据达到了这个日期的分片后后循环从开始分片插入。
Assert.assertEquals(true, 0 == partition.calculate("2014-01-01")); 
Assert.assertEquals(true, 0 == partition.calculate("2014-01-10")); 
Assert.assertEquals(true, 1 == partition.calculate("2014-01-11")); 
Assert.assertEquals(true, 12 == partition.calculate("2014-05-01"));

2.4.6. 取模范围约束

此种规则是取模运算与范围约束的结合,主要为了后续数据迁移做准备,即可以自主决定取模后数据的节点分布。

<tableRule name="sharding-by-pattern"> 
<rule> 
<columns>user_id</columns> 
<algorithm>sharding-by-pattern</algorithm> 
</rule> 
</tableRule> 

<function name="sharding-by-pattern" class="io.mycat.route.function.PartitionByPattern"
<property name="patternValue">256</property> 
<property name="defaultNode">2</property> 
<property name="mapFile">partition-pattern.txt</property> 
</function> 

partition-pattern.txt 配置:

# id partition range start-end, data node index 
###### first host configuration 
1-32=0 
33-64=1 
65-96=2 
97-128=3 
######## second host configuration 
129-160=4 
161-192=5 
193-224=6 
225-256=7 
0-0=7  

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数,patternValue 即求模基数,defaoultNode 默认节点,如果配置了默认,则不会按照求模运算。mapFile 配置文件路径配置文件中,1-32 即代表 id%256 后分布的范围,如果在 1-32 则在分区 1,其他类推,如果 id 非数据,则会分配在 defaoultNode 默认节点。

String idVal = "0"; 
Assert.assertEquals(true, 7 == autoPartition.calculate(idVal)); 
idVal = "45a"; 
Assert.assertEquals(true, 2 == autoPartition.calculate(idVal)); 

2.4.7. 截取数字做 hash 求模范围约束

此种规则类似于取模范围约束,此规则支持数据符号字母取模。

<tableRule name="sharding-by-prefixpattern"> 
<rule> 
<columns>user_id</columns> 
<algorithm>sharding-by-prefixpattern</algorithm> 
</rule> 
</tableRule> 

<function name="sharding-by-prefixpattern" class="io.mycat.route.function.PartitionByPrefixPattern"> 
<property name="patternValue">256</property> 
<property name="prefixLength">5</property> 
<property name="mapFile">partition-pattern.txt</property> 
</function>  

partition-pattern.txt 配置:

# range start-end ,data node index 
# ASCII 
# 8-57=0-9 阿拉伯数字 
# 64、65-90=@、A-Z 
# 97-122=a-z 
 
###### first host configuration 
1-4=0 
5-8=1 
9-12=2 
13-16=3 
###### second host configuration 
17-20=4 
21-24=5 
25-28=6 
29-32=7 
0-0=7  

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数,patternValue 即求模基数,prefixLength ASCII 截取的位数。mapFile 配置文件路径 配置文件中,1-32 即代表 id%256 后分布的范围,如果在 1-32 则在分区 1,其他类推 此种方式类似方式 6 只不过采取的是将列种获取前 prefixLength 位列所有 ASCII 码的和进行求模 sum%patternValue ,获取的值,在范围内的分片数。

String idVal="gf89f9a"; 
Assert.assertEquals(true, 0==autoPartition.calculate(idVal)); 
idVal="8df99a"; 
Assert.assertEquals(true, 4==autoPartition.calculate(idVal));
idVal="8dhdf99a"; 
Assert.assertEquals(true, 3==autoPartition.calculate(idVal)); 

2.4.8. 应用指定

此规则是在运行阶段有应用自主决定路由到那个分片。

<tableRule name="sharding-by-substring"> 
<rule> 
<columns>user_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><!-- zero-based --> 
<property name="size">2</property> 
<property name="partitionCount">8</property> 
<property name="defaultPartition">0</property> 
</function> 

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数 此方法为直接根据字符子串(必须是数字)计算分区号(由应用传递参数,显式指定分区号)。 例如 id=05-100000002 在此配置中代表根据 id 中从 startIndex=0,开始,截取 siz=2 位数字即 05,05 就是获取的分区,如果没传默认分配到 defaultPartition。

2.4.9. 截取数字 hash 解析

此规则是截取字符串中的 int 数值 hash 分片。

<tableRule name="sharding-by-stringhash"> 
<rule> 
<columns>user_id</columns> 
<algorithm>sharding-by-stringhash</algorithm> 
</rule> 
</tableRule> 
<function name="sharding-by-stringhash" class="io.mycat.route.function.PartitionByString"> 
<property name="partitionLength">512</property><!-- zero-based --> 
<property name="partitionCount">2</property> 
<property name="hashSlice">0:2</property> 
</function> 

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数函数中 partitionLength 代表字符串 hash 求模基数, partitionCount 分区数, hashSlice hash 预算位,即根据子字符串中 int 值 hash 运算。

hashSlice : 0 代表 str.length(), -1 代表 str.length()-1;

2-> (0,2) 
“1:2-> (1,2) 
“1:” -> (1,0) 
“-1:” -> (-1,0) 
“:-1-> (0,-1) 
“:” -> (0,0) 

2.4.10. 一致性 hash

一致性 hash 预算有效解决了分布式数据的扩容问题。

<tableRule name="sharding-by-murmur"> 
<rule> 
<columns>user_id</columns> 
<algorithm>murmur</algorithm> 
</rule> 
</tableRule> 

<function name="murmur" class="io.mycat.route.function.PartitionByMurmurHash"> 
<property name="seed">0</property><!-- 默认是 0--> 
<property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片--> 
<property name="virtualBucketTimes">160</property><!-- 一个实际的数据库节点被映射为这么多虚拟 
节点,默认是 160 倍,也就是虚拟节点数是物理节点数的 160 倍--> 
<!-- 
<property name="weightMapFile">weightMapFile</property> 
节点的权重,没有指定权重的节点默认是 1。以 properties 文件的格式填写,以从 0 开始到 count-1 的整数值也就是节点索引为 key,以节点权重值为值。所有权重值必须是正整数,否则以 1 代替 --> 
<!-- 
<property name="bucketMapPath">/etc/mycat/bucketMapPath</property>
用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,会把虚拟节点的 murmur hash 值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,就不会输出任何东西 --> 
</function>  

2.4.11. 按单月小时拆分

此规则是单月内按照小时拆分,最小粒度是小时,可以一天最多 24 个分片,最少 1 个分片,一个月完后下月从头开始循环。 每个月月尾,需要手工清理数据。

<tableRule name="sharding-by-hour"> 
<rule> 
<columns>create_time</columns> 
<algorithm>sharding-by-hour</algorithm> 
</rule> 
</tableRule> 

<function name="sharding-by-hour" class="io.mycat.route.function.LatestMonthPartion"> 
<property name="splitOneDay">24</property> 
</function>  

配置说明

  • columns: 拆分字段,字符串类型(yyyymmddHH)
  • splitOneDay : 一天切分的分片数

2.4.12. 范围求模分片

先进行范围分片计算出分片组,组内再求模优点可以避免扩容时的数据迁移,又可以一定程度上避免范围分片的热点问题。 综合了范围分片和求模分片的优点,分片组内使用求模可以保证组内数据比较均匀,分片组之间是范围分片可以兼顾范围查询。

最好事先规划好分片的数量,数据扩容时按分片组扩容,则原有分片组的数据不需要迁移。由于分片组内数据比较均匀,所以分片组内可以避免热点数据问题。

<tableRule name="auto-sharding-rang-mod"> 
<rule> 
<columns>id</columns> 
<algorithm>rang-mod</algorithm> 
</rule> 
</tableRule> 

<function name="rang-mod" class="io.mycat.route.function.PartitionByRangeMod"> 
<property name="mapFile">partition-range-mod.txt</property> 
<property name="defaultNode">21</property> 
</function> 

配置说明

上面 columns 标识将要分片的表字段,algorithm 分片函数, rang-mod 函数中 mapFile 代表配置文件路径 defaultNode 超过范围后的默认节点顺序号,节点从 0 开始。以下配置一个范围代表一个分片组,=号后面的数字代表该分片组所拥有的分片的数量。

partition-range-mod.txt 配置:

0-200M=5  //代表有 5 个分片节点 
200M1-400M=1 
400M1-600M=4 
600M1-800M=4 
800M1-1000M=6 

2.4.13. 日期范围 hash 分片

思想与范围求模一致,当由于日期在取模会有数据集中问题,所以改成 hash 方法。先根据日期分组,再根据时间 hash 使得短期内数据分布的更均匀优点可以避免扩容时的数据迁移,又可以一定程度上避免范围分片的热点问题要求日期格式尽量精确些,不然达不到局部均匀的目的。

<tableRule name="rangeDateHash"> 
<rule> 
<columns>col_date</columns> 
<algorithm>range-date-hash</algorithm> 
</rule> 
</tableRule> 

<function name="range-date-hash" class="io.mycat.route.function.PartitionByRangeDateHash"> 
<property name="sBeginDate">2014-01-01 00:00:00</property> <property name="sPartionDay">3</property> 
<property name="dateFormat">yyyy-MM-dd HH:mm:ss</property> 
<property name="groupPartionSize">6</property> 
</function> 

sPartionDay 代表多少天分一个分片 groupPartionSize 代表分片组的大小。

2.4.14. 冷热数据分片

根据日期查询日志数据 冷热数据分布 ,最近 n 个月的到实时交易库查询,超过 n 个月的按照 m 天分片。

<tableRule name="sharding-by-date"> 
<rule> 
<columns>create_time</columns> 
<algorithm>sharding-by-hotdate</algorithm> 
</rule> 
</tableRule> 

<function name="sharding-by-hotdate" class="io.mycat.route.function.PartitionByHotDate"> 
<property name="dateFormat">yyyy-MM-dd</property> 
<property name="sLastDay">10</property> 
<property name="sPartionDay">30</property> 
</function> 

2.4.15. 自然月分片

按月份列分区 ,每个自然月一个分片,格式 between 操作解析的范例。

<tableRule name="sharding-by-month"> 
<rule> 
<columns>create_time</columns> 
<algorithm>sharding-by-month</algorithm> 
</rule> 
</tableRule> 

<function name="sharding-by-month" class="io.mycat.route.function.PartitionByMonth"> 
<property name="dateFormat">yyyy-MM-dd</property> 
<property name="sBeginDate">2014-01-01</property> 
</function>  

配置说明

  • columns: 分片字段,字符串类型
  • dateFormat : 日期字符串格式
  • sBeginDate : 开始日期
PartitionByMonth partition = new PartitionByMonth(); 
partition.setDateFormat("yyyy-MM-dd"); 
partition.setsBeginDate("2014-01-01"); 
partition.init(); 
Assert.assertEquals(true, 0 == partition.calculate("2014-01-01")); 
Assert.assertEquals(true, 0 == partition.calculate("2014-01-10")); 
Assert.assertEquals(true, 0 == partition.calculate("2014-01-31")); 
Assert.assertEquals(true, 1 == partition.calculate("2014-02-01")); 
Assert.assertEquals(true, 1 == partition.calculate("2014-02-28")); 
Assert.assertEquals(true, 2 == partition.calculate("2014-03-1")); 
Assert.assertEquals(true, 11 == partition.calculate("2014-12-31")); 
Assert.assertEquals(true, 12 == partition.calculate("2015-01-31")); 
Assert.assertEquals(true, 23 == partition.calculate("2015-12-31")); 

2.4.16. crc32slot 分片算法

有状态分片算法与之前的分片算法不同,它是为数据自动迁移而设计的。

crc32solt 是有状态分片算法的实现之一, 数据自动迁移方案设计 crc32(key)%102400=slot;

slot 按照范围均匀分布在 dataNode 上,针对每张表进行实例化,通过一个文件记录 slot 和节点 映射关系,迁移过程中通过 zk 协调其中需要在分片表中增加 slot 字段,用以避免迁移时重新计算,只需要迁移对应 slot 数据即可。分片最大个数为 102400 个,短期内应该够用,每分片一千万,总共可以支持一万亿数据。

<table name="travelrecord" dataNode="dn1,dn2" rule="crc32slot" /> 

2.5. Mycat 多租户支持

单租户就是传统的给每个租户独立部署一套 web + db 。由于租户越来越多,整个 web 部分的机器和运维成本都非常高,因此需要改进到所有租户共享一套 web 的模式(db 部分暂不改变)。

基于此需求,我们对单租户的程序做了简单的改造实现 web 多租户共享。具体改造如下:

  1. web 部分修改:

    • 在用户登录时,在线程变量(ThreadLocal)中记录租户的 id;
    • 修改 jdbc 的实现:在提交 sql 时,从 ThreadLocal 中获取租户 id, 添加 sql 注释,把租户的 schema 放到注释中。例如:/*!mycat : schema = test_01 */ sql ;
  2. 在 db 前面建立 proxy 层,代理所有 web 过来的数据库请求。proxy 层是用 mycat 实现的,web 提交的 sql 过来时在注释中指定 schema, proxy 层根据指定的 schema 转发 sql 请求。

  3. Mycat 配置:

<user name="mycat"> 
<property name="password">mycat</property> 
<property name="schemas">order</property> 
<property name="readOnly">true</property> 
</user> 
 
<user name="mycat2"> 
<property name="password">mycat</property> 
<property name="schemas">order</property> 
</user> 

分库分表解决方案 MyCat 系列 数据切分篇
分库分表解决方案 MyCat 系列 基本概念篇
分库分表解决方案 MyCat 系列 数据分片篇
分库分表解决方案 MyCat 系列 集群事务篇
分库分表解决方案 MyCat 系列 路由分发篇