10、Mycat实战:分表实现

文章目录

  • 12.分表
    • 12.1.需求
  • 12.2分表实现(取模)
    • 1.原则
    • 2.安装数据库
    • 3.创建数据库、表
    • 4.mycat实现分表
  • 12.3 Mycat 的分片 join
  • 12.4 ER分片
  • 12.5 全局表
    • 1.介绍
    • 2.实现
    • 3.验证全局表
    1. 其它分片规则
    • 1.取 模
  • 2.分片枚举
    • 1.配置schema.xml
    • 2.修改rule.xml
  • 3.范围约定
      1. 验证
    • 2.修改schema.xml
    • 3.修改rule.xml
    • 4.编辑autopartition-long.txt
    • 5.插入数据
    • 6.查询
  • 4.按日期(天、月)分片
    • 1.修改schema.xml
    • 2.修改rule.xml
    • 3.重启mycat,插入数据测试
    1. 固定分片 hash 算法
  • 6.取模范围约束
  • 7.截取数字做 hash 求模范围约束
  • 8.应用指定
  • 9.截取数字 hash 解析
  • 10.一致性 hash
  • 11.按单月小时拆分
  • 12.范围求模分片
    1. 日期范围 hash 分片
  • 14.冷热数据分片

12.分表

相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中 包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分 到一个数据库,而另外的某些行又切分到其他的数据库中。

12.1.需求

在电信行业有电信计费系统(BOSS系统),假设其中存储如下信息:

  • 客户手机账户(手机号)信息
  • 手机通话记录信息
  • 字典表(如存储常用的码表信息,例如 通话类型,01:呼出,02:呼入等)

简单ER图如下:

*

MySQL 单表存储数据条数是有瓶颈的,单表达到 1000 万条数据就达到了瓶颈,会影响查询效率,

需要进行水平拆分(分表)进行优化。BOSS系统预测5年内客户手机账户表5000万条以上。解决方案是将手机号表进行水平拆分。

表结构sql语句如下:

/* 创建数据库 */
CREATE DATABASE boss;

USE boss;

/* 客户手机号表 */
CREATE TABLE customer (
  id bigint(20) NOT NULL COMMENT '主键',
  cid bigint(20) DEFAULT NULL COMMENT '客户id',
  name varchar(500) DEFAULT NULL COMMENT '客户名称',
  phone varchar(500) DEFAULT NULL COMMENT '电话号',
  provice varchar(500) DEFAULT NULL COMMENT '所属省份',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电信客户信息(手机号)';
/* 手机号通话记录 */
CREATE TABLE calllog (
  id bigint(20) NOT NULL COMMENT 'id',
  phone_id bigint(20) DEFAULT NULL COMMENT '主键',
  type varchar(10) DEFAULT NULL COMMENT '通话类型',
  duration bigint(20) DEFAULT NULL COMMENT '通话时长(秒)',
  othernum varchar(20) DEFAULT NULL COMMENT '对方电话号',
  PRIMARY KEY (id),
  KEY FK_Reference_1 (phone_id),
  CONSTRAINT FK_Reference_1 FOREIGN KEY (phone_id) REFERENCES customer (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话记录';
/* 字典表 */
CREATE TABLE dict (
  id bigint(20) NOT NULL COMMENT 'id',
  caption varchar(100) DEFAULT NULL COMMENT '代码类型名称',
  code varchar(10) DEFAULT NULL COMMENT '代码',
  name varchar(10) DEFAULT NULL COMMENT '名称',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典表';

12.2分表实现(取模)

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

<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,此种在批量插入时可能存在批量插入单

事务插入多数据分片,增大事务一致性难度(因此种方式实现最简单所以优先说明)。

1.原则

以客户表(customer)为例可以采用不同字段进行分表:

序号 分表字段 说明
1 id(主键、或创建时间) 从业务上来看同一个客户的不同手机号分布在不同的数据节点上,可能造成查询效率降低
2 cid(客户 id)、provice(省份) 根据客户 id 去分,两个节点访问平均,一个客户所有的手机号都在同一个数据节点上。

2.安装数据库

此处同上述过程一样采用docker容器模拟不同的数据节点。此处为测试方便创建两个MySQL数据库容器。

docker run --name spt1 -p 3416:3306   -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7 &&\

docker run --name spt2 -p 3426:3306   -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

执行过程如下:

[root@mycat ~]# docker run --name spt1 -p 3416:3306   -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7 && docker run --name spt2 -p 3426:3306   -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
c9273c38f676aaf09321c6b117cf9445d5a15a632694480daf02db8cc9352bf6
5b19f22dbefbd00a65aaa6185a0c493518b4d71a5ff10b63f0bef6404efbb9bc
[root@mycat ~]# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
5b19f22dbefb   mysql:5.7   "docker-entrypoint.s…"   4 seconds ago   Up 2 seconds   33060/tcp, 0.0.0.0:3426->3306/tcp, :::3426->3306/tcp   spt2
c9273c38f676   mysql:5.7   "docker-entrypoint.s…"   6 seconds ago   Up 4 seconds   33060/tcp, 0.0.0.0:3416->3306/tcp, :::3416->3306/tcp   spt1
[root@mycat ~]# 

3.创建数据库、表

分别连接两个容器,并执行上述sql脚本用于创建数据库、数据库表。

  • 查询两个容器的ip地址
[root@mycat ~]# docker inspect --format '{
   
     { .NetworkSettings.IPAddress }}'  spt1 | awk '{print "spt1:",$1}' && docker inspect --format '{
   
     { .NetworkSettings.IPAddress }}'  spt2 | awk '{print "spt2:",$1}'

spt1: 172.17.0.2
spt2: 172.17.0.3
[root@mycat ~]# 
  • 连接容器spt1,执行数据库脚本
[root@mycat ~]# mysql -uroot -proot -h172.17.0.2 -P 3306
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.34 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> /* 创建数据库 */
MySQL [(none)]> CREATE DATABASE boss;
Query OK, 1 row affected (0.00 sec)

MySQL [(none)]> 
MySQL [(none)]> USE boss;
 phone varchar(500) DEFAULT NULL COMMENT '电话号',
  provice varchar(500) DEFAULT NULL COMMENT '所属省份',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电信客户信息(手机号)';
/* 手机号通话记录 */
CREATE TABLE calllog (
  id bigint(20) NOT NULL COMMENT 'id',
  phone_id bigint(20) DEFAULT NULL COMMENT '客户手机号外键',
  type varchar(10) DEFAULT NULL COMMENT '通话类型',
  duration bigint(20) DEFAULT NULL COMMENT '通话时长(秒)',
  othernum varchar(20) DEFAULT NULL COMMENT '对方电话号',
  PRIMARY KEY (id),
  KEY FK_Reference_1 (phone_id),
  CONSTRAINT FK_Reference_1 FOREIGN KEY (phone_id) REFERENCES customer (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话记录';
/* 字典表 */
CREATE TABLE dict (
  id bigint(20) NOT NULL COMMENT 'id',
  caption varchar(100) DEFAULT NULL COMMENT '代码类型名称',
  code varchar(10) DEFAULT NULL COMMENT '代码',
  name varchar(10) DEFAULT NULL COMMENT '名称',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFDatabase changed
MySQL [boss]> 
MySQL [boss]> /* 客户手机号表 */
MySQL [boss]> CREATE TABLE customer (
    ->   id bigint(20) NOT NULL COMMENT '主键',
    ->   cid bigint(20) DEFAULT NULL COMMENT '客户id',
    ->   name varchar(500) DEFAULT NULL COMMENT '客户名称',
    ->   phone varchar(500) DEFAULT NULL COMMENT '电话号',
    ->   provice varchar(500) DEFAULT NULL COMMENT '所属省份',
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电信客户信息(手机号)';
AULT CHARSET=utf8mb4 COMMENT='字典表';Query OK, 0 rows affected (0.04 sec)

MySQL [boss]> 
MySQL [boss]> 
MySQL [boss]> /* 手机号通话记录 */
MySQL [boss]> CREATE TABLE calllog (
    ->   id bigint(20) NOT NULL COMMENT 'id',
    ->   phone_id bigint(20) DEFAULT NULL COMMENT '主键',
    ->   type varchar(10) DEFAULT NULL COMMENT '通话类型',
    ->   duration bigint(20) DEFAULT NULL COMMENT '通话时长(秒)',
    ->   othernum varchar(20) DEFAULT NULL COMMENT '对方电话号',
    ->   PRIMARY KEY (id),
    ->   KEY FK_Reference_1 (phone_id),
    ->   CONSTRAINT FK_Reference_1 FOREIGN KEY (phone_id) REFERENCES customer (id)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话记录';
Query OK, 0 rows affected (0.03 sec)

MySQL [boss]> 
MySQL [boss]> 
MySQL [boss]> /* 字典表 */
MySQL [boss]> CREATE TABLE dict (
    ->   id bigint(20) NOT NULL COMMENT 'id',
    ->   caption varchar(100) DEFAULT NULL COMMENT '代码类型名称',
    ->   code varchar(10) DEFAULT NULL COMMENT '代码',
    ->   name varchar(10) DEFAULT NULL COMMENT '名称',
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典表';
Query OK, 0 rows affected (0.02 sec)

MySQL [boss]> show tables;
+----------------+
| Tables_in_boss |
+----------------+
| calllog        |
| customer       |
| dict           |
+----------------+
3 rows in set (0.00 sec)

MySQL [boss]> 
  • 连接容器spt1,执行数据库脚本
[root@mycat ~]# mysql -uroot -proot -h172.17.0.3 -P 3306
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.34 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> /* 创建数据库 */
MySQL [(none)]> CREATE DATABASE boss;
Query OK, 1 row affected (0.00 sec)

MySQL [(none)]> 
MySQL [(none)]> USE boss;
 phone varchar(500) DEFAULT NULL COMMENT '电话号',
  provice varchar(500) DEFAULT NULL COMMENT '所属省份',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电信客户信息(手机号)';
/* 手机号通话记录 */
CREATE TABLE calllog (
  id bigint(20) NOT NULL COMMENT 'id',
  phone_id bigint(20) DEFAULT NULL COMMENT '主键',
  type varchar(10) DEFAULT NULL COMMENT '通话类型',
  duration bigint(20) DEFAULT NULL COMMENT '通话时长(秒)',
  othernum varchar(20) DEFAULT NULL COMMENT '对方电话号',
  PRIMARY KEY (id),
  KEY FK_Reference_1 (phone_id),
  CONSTRAINT FK_Reference_1 FOREIGN KEY (phone_id) REFERENCES customer (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话记录';
/* 字典表 */
CREATE TABLE dict (
  id bigint(20) NOT NULL COMMENT 'id',
  caption varchar(100) DEFAULT NULL COMMENT '代码类型名称',
  code varchar(10) DEFAULT NULL COMMENT '代码',
  name varchar(10) DEFAULT NULL COMMENT '名称',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFDatabase changed
MySQL [boss]> 
MySQL [boss]> /* 客户手机号表 */
MySQL [boss]> CREATE TABLE customer (
    ->   id bigint(20) NOT NULL COMMENT '主键',
    ->   cid bigint(20) DEFAULT NULL COMMENT '客户id',
    ->   name varchar(500) DEFAULT NULL COMMENT '客户名称',
    ->   phone varchar(500) DEFAULT NULL COMMENT '电话号',
    ->   provice varchar(500) DEFAULT NULL COMMENT '所属省份',
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电信客户信息(手机号)';
AULT CHARSET=utf8mb4 COMMENT='字典表';Query OK, 0 rows affected (0.04 sec)

MySQL [boss]> 
MySQL [boss]> 
MySQL [boss]> /* 手机号通话记录 */
MySQL [boss]> CREATE TABLE calllog (
    ->   id bigint(20) NOT NULL COMMENT 'id',
    ->   phone_id bigint(20) DEFAULT NULL COMMENT '主键',
    ->   type varchar(10) DEFAULT NULL COMMENT '通话类型',
    ->   duration bigint(20) DEFAULT NULL COMMENT '通话时长(秒)',
    ->   othernum varchar(20) DEFAULT NULL COMMENT '对方电话号',
    ->   PRIMARY KEY (id),
    ->   KEY FK_Reference_1 (phone_id),
    ->   CONSTRAINT FK_Reference_1 FOREIGN KEY (phone_id) REFERENCES customer (id)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通话记录';
Query OK, 0 rows affected (0.03 sec)

MySQL [boss]> 
MySQL [boss]> 
MySQL [boss]> /* 字典表 */
MySQL [boss]> CREATE TABLE dict (
    ->   id bigint(20) NOT NULL COMMENT 'id',
    ->   caption varchar(100) DEFAULT NULL COMMENT '代码类型名称',
    ->   code varchar(10) DEFAULT NULL COMMENT '代码',
    ->   name varchar(10) DEFAULT NULL COMMENT '名称',
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典表';
Query OK, 0 rows affected (0.02 sec)

MySQL [boss]> show tables;
+----------------+
| Tables_in_boss |
+----------------+
| calllog        |
| customer       |
| dict           |
+----------------+
3 rows in set (0.00 sec)

MySQL [boss]> 

4.mycat实现分表

根据需求此处需要将customer 进行水平拆分,并分布到两个数据节点spt1、spt2上。需要做如下修改

  • 修改schema.xml
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
    
	<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="spt1">
		<!-- 
			rule="customer_rule"  使用的 分片规则,需要在rule.xml中配置
		-->
        <table name="customer" dataNode="spt1,spt2" rule="customer_rule" ></table>
    </schema>

    <dataNode name="spt1" dataHost="host1" database="boss"/>
    <dataNode name="spt2" dataHost="host2" database="boss"/>

    <dataHost name="host1" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM1" url="172.17.0.2:3306" user="root" password="root">
        </writeHost>
    </dataHost>
	
    <dataHost name="host2" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM2" url="172.17.0.3:3306" user="root"  password="root">
        </writeHost>
    </dataHost>

</mycat:schema>

  • 修改rule.xml配置customer_rule规则

在rule 配置文件里新增分片规则 customer_rule,并指定规则适用字段为cid,

<tableRule name="customer_rule">
    <rule>
        <!-- 分片字段-->
         <columns>cid</columns>
        <!-- 分片算法名 -->
         <algorithm>mod-long</algorithm>
    </rule>
 </tableRule>

还有选择分片算法 mod-long(对字段求模运算),cid对两个节点求模,根据结果分片

配置算法 mod-long 参数 count 为 2,两个节点

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

配置完的完整的rule.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<!-- - - Licensed under the Apache License, Version 2.0 (the "License"); 
	- you may not use this file except in compliance with the License. - You 
	may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 
	- - Unless required by applicable law or agreed to in writing, software - 
	distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 
	WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the 
	License for the specific language governing permissions and - limitations 
	under the License. -->
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/">
	<tableRule name="customer_rule">
                <rule>
                        <columns>cid</columns>
                        <algorithm>mod-long</algorithm>
                </rule>
        </tableRule>
	<tableRule name="rule1">
		<rule>
			<columns>id</columns>
			<algorithm>func1</algorithm>
		</rule>
	</tableRule>

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

	<tableRule name="sharding-by-intfile">
		<rule>
			<columns>sharding_id</columns>
			<algorithm>hash-int</algorithm>
		</rule>
	</tableRule>
	<tableRule name="auto-sharding-long">
		<rule>
			<columns>id</columns>
			<algorithm>rang-long</algorithm>
		</rule>
	</tableRule>
	<tableRule name="mod-long">
		<rule>
			<columns>id</columns>
			<algorithm>mod-long</algorithm>
		</rule>
	</tableRule>
	<tableRule name="sharding-by-murmur">
		<rule>
			<columns>id</columns>
			<algorithm>murmur</algorithm>
		</rule>
	</tableRule>
	<tableRule name="crc32slot">
		<rule>
			<columns>id</columns>
			<algorithm>crc32slot</algorithm>
		</rule>
	</tableRule>
	<tableRule name="sharding-by-month">
		<rule>
			<columns>create_time</columns>
			<algorithm>partbymonth</algorithm>
		</rule>
	</tableRule>
	<tableRule name="latest-month-calldate">
		<rule>
			<columns>calldate</columns>
			<algorithm>latestMonth</algorithm>
		</rule>
	</tableRule>
	
	<tableRule name="auto-sharding-rang-mod">
		<rule>
			<columns>id</columns>
			<algorithm>rang-mod</algorithm>
		</rule>
	</tableRule>
	
	<tableRule name="jch">
		<rule>
			<columns>id</columns>
			<algorithm>jump-consistent-hash</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>

	<function name="crc32slot"
			  class="io.mycat.route.function.PartitionByCRC32PreSlot">
		<property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
	</function>
	<function name="hash-int"
		class="io.mycat.route.function.PartitionByFileMap">
		<property name="mapFile">partition-hash-int.txt</property>
	</function>
	<function name="rang-long"
		class="io.mycat.route.function.AutoPartitionByLong">
		<property name="mapFile">autopartition-long.txt</property>
	</function>
	<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
		<!-- how many data nodes -->
		<property name="count">2</property>
	</function>

	<function name="func1" class="io.mycat.route.function.PartitionByLong">
		<property name="partitionCount">8</property>
		<property name="partitionLength">128</property>
	</function>
	<function name="latestMonth"
		class="io.mycat.route.function.LatestMonthPartion">
		<property name="splitOneDay">24</property>
	</function>
	<function name="partbymonth"
		class="io.mycat.route.function.PartitionByMonth">
		<property name="dateFormat">yyyy-MM-dd</property>
		<property name="sBeginDate">2015-01-01</property>
	</function>
	
	<function name="rang-mod" class="io.mycat.route.function.PartitionByRangeMod">
        	<property name="mapFile">partition-range-mod.txt</property>
	</function>
	
	<function name="jump-consistent-hash" class="io.mycat.route.function.PartitionByJumpConsistentHash">
		<property name="totalBuckets">3</property>
	</function>
</mycat:rule>

启动Mycat,登录Mycat插入数据进行验证

insert into customer (id, cid, name, phone, provice) values('1','1','张飞','13800000001','燕人');
insert into customer (id, cid, name, phone, provice) values('2','2','赵云','13800000002','真定');
insert into customer (id, cid, name, phone, provice) values('3','3','诸葛亮','13800000003','沂南');
insert into customer (id, cid, name, phone, provice) values('4','4','关羽','13800000004','运城');
insert into customer (id, cid, name, phone, provice) values('5','5','刘玄德','13800000005','涿州');
insert into customer (id, cid, name, phone, provice) values('6','6','孙策','13800000006','东吴');

插入数据后在Mycat客户端、数据节点spt1、sp2上分别执行查询发现

  • 数据节点spt1、spt2分别保存一部分数据
  • mycat客户端可以查询出所有数据
    *

需要注意的点,mycat插入时根据字段分片,需要提供字段枚举

正确的插入sql

insert into customer (id, cid, name, phone, provice) values('5','5','刘玄德','13800000005','涿州');

错误的sql

insert into customer values('6','6','孙策','13800000006','东吴');

不提供列的枚举会报如下错误

MySQL [TESTDB]> insert into customer  values('6','6','孙策','13800000006','东吴');
ERROR 1064 (HY000): partition table, insert must provide ColumnList

12.3 Mycat 的分片 join

Join 绝对是关系型数据库中最常用一个特性,然而在分布式环境中,跨分片的 join 确是最复杂的,最难解决一

个问题。

Mycat性能建议

尽量避免使用 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 支

持。

12.4 ER分片

MyCAT 借鉴了 NewSQL 领域的新秀 Foundation DB 的设计思路,Foundation DB 创新性的提出了 Table

Group 的概念,其将子表的存储位置依赖于主表,并且物理上紧邻存放,因此彻底解决了 JION 的效率和性能问

题,根据这一思路,提出了基于 E-R 关系的数据分片策略,子表的记录与所关联的父表记录存放在同一个数据分

片上。

customer(客户手机账户表) 采用 按照客户id取模 这个分片策略,分片在 spt1,spt2 上,calllog(呼叫记录表) 依赖父表进行分片,两个

表的关联关系为 customer.id =calllog.phone_id。于是数据分片和存储的示意图如下:
*

这样一来,分片 spt1 上的的 customer 与 spt1 上的 calllog 就可以进行局部的 JOIN 联合,spt2(…sptn) 上也如此,再合并两个节点的数据即可完成整体的 JOIN,基于 E-R 映射的数据分片模式,基本上解决了 80%以上的企业应用所面临的问题。

实现

修改schema.xml文件,在上次配置基础上修改table标签,在customer 表下添加子标签

<childTable name="calllog" primaryKey="id" joinKey="id" parentKey="phone_id" />

完整的schema.xml文件如下:

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
    
	<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="spt1">
		<!-- 
		     			rule="customer_rule"  自定义的 分片规则,需要在rule.xml中配置
		-->
        <table name="customer" dataNode="spt1,spt2" rule="customer_rule" >
			<!-- 
				name="calllog"  		子表(从表)名称
				primaryKey="id" 		子表(从表)主键
				joinKey="phone_id"    	从表中记录的外键(calllog.phone_id)
				parentKey="id"			主表主键(customer.id)
			-->
			<childTable name="calllog" primaryKey="id" joinKey="phone_id" parentKey="id" />
		</table>
    </schema>

    <dataNode name="spt1" dataHost="host1" database="boss"/>
    <dataNode name="spt2" dataHost="host2" database="boss"/>

    <dataHost name="host1" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM1" url="172.17.0.2:3306" user="root" password="root">
        </writeHost>
    </dataHost>
	
    <dataHost name="host2" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM2" url="172.17.0.3:3306" user="root"  password="root">
        </writeHost>
    </dataHost>

</mycat:schema>

重新启动mycat(加载配置),登录mycat插入数据

insert into calllog (id, phone_id, type, duration, othernum) values('1','1','01','77','15800000001');
insert into calllog (id, phone_id, type, duration, othernum) values('2','1','02','45','15800000002');
insert into calllog (id, phone_id, type, duration, othernum) values('3','2','01','38','15800000003');
insert into calllog (id, phone_id, type, duration, othernum) values('4','2','02','64','15800000004');
insert into calllog (id, phone_id, type, duration, othernum) values('5','3','01','57','15800000005');
insert into calllog (id, phone_id, type, duration, othernum) values('6','3','02','88','15800000006');
insert into calllog (id, phone_id, type, duration, othernum) values('7','4','01','88','15800000007');
insert into calllog (id, phone_id, type, duration, othernum) values('8','4','02','69','15800000008');
insert into calllog (id, phone_id, type, duration, othernum) values('9','5','01','23','15800000009');
insert into calllog (id, phone_id, type, duration, othernum) values('10','5','02','46','15800000010');
insert into calllog (id, phone_id, type, duration, othernum) values('11','6','01','45','15800000011');
insert into calllog (id, phone_id, type, duration, othernum) values('12','6','02','77','15800000012');

分别在mycat、数据节点spt1、spt2使用join查询

SELECT 
  cus.id,
  cus.name '客户名称',
  log.type '通话类型',
  log.othernum '对方电话号' 
FROM
  customer cus 
  INNER JOIN calllog log
    ON cus.id = log.phone_id 
ORDER BY cus.id ;

*

12.5 全局表

1.介绍

前几章节已经实现了表的的水平划分(分表)。上述表中还有一个dict(字典表)存储的是一些系统中常用的数据字典,如代表通话类型的代码(01:呼入,02:呼出),这张表如果单独在某一个数据节点(如:spt1)上,会导致另外数据节点(如:spt2)上的数据无法关联查询

一个真实的业务系统中,往往存在大量的类似字典表的表,这些表基本上很少变动,字典表具有以下几个特

性:

•变动不频繁;

•数据量总体变化不大;

•数据规模不大,很少有超过数十万条记录。

对于这类的表,在分片的情况下,当业务表因为规模而进行分片以后,业务表与这些附属的字典表之间的关

联,就成了比较棘手的问题,所以 Mycat 中通过数据冗余来解决这类表的 join,即所有的分片都有一份数据的拷

贝,所有将字典表或者符合字典表特性的一些表定义为全局表。

数据冗余是解决跨分片数据 join 的一种很好的思路,也是数据切分规划的另外一条重要规则。

2.实现

修改schema.xml 配置文件,添加table节点并设置为global类型

<table name="dict" dataNode="spt1,spt2" type="global" ></table>

全量配置文件如下:

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
    
	<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="spt1">
		<!-- 
		     			rule="customer_rule"  自定义的 分片规则,需要在rule.xml中配置
		-->
        <table name="customer" dataNode="spt1,spt2" rule="customer_rule" >
			<!-- 
				name="calllog"  		子表(从表)名称
				primaryKey="id" 		子表(从表)主键
				joinKey="phone_id"    	从表中记录的外键(calllog.phone_id)
				parentKey="id"		    主表主键(customer.id)
			-->
			<childTable name="calllog" primaryKey="id" joinKey="phone_id" parentKey="id" />
		</table>
		
		<!-- 
			设置 dict表为  global全局表,并在spt1、spt2两个数据节点冗余(内容重复)存在
		-->
		<table name="dict" dataNode="spt1,spt2" type="global" ></table>
		
    </schema>

    <dataNode name="spt1" dataHost="host1" database="boss"/>
    <dataNode name="spt2" dataHost="host2" database="boss"/>

    <dataHost name="host1" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM1" url="172.17.0.2:3306" user="root" password="root">
        </writeHost>
    </dataHost>
	
    <dataHost name="host2" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM2" url="172.17.0.3:3306" user="root"  password="root">
        </writeHost>
    </dataHost>

</mycat:schema>

3.验证全局表

登录mycat插入字典表的数据

insert into dict (id, caption, code, name) values('1','ptype','01','呼出');
insert into dict (id, caption, code, name) values('2','ptype','02','呼入');

分别在mycat客户端、数据节点spt1、spt2中查询关联dict表的sql

SELECT 
  cus.id,
  cus.name '客户名称',
  dict.name '通话类型', 
  log.othernum '对方电话号' 
FROM
  customer cus 
  INNER JOIN calllog log
    ON cus.id = log.phone_id 
  INNER JOIN dict ON dict.caption='ptype' AND dict.code = log.type
ORDER BY cus.id ;

*

13. 其它分片规则

常用的数据库分片规则在mycat中有如下几种实现

PartitionByCRC32PreSlot.java
PartitionByDate.java
PartitionByFileMap.java
PartitionByHashMod.java
PartitionByHotDate.java
PartitionByJumpConsistentHash.java
PartitionByLong.java
PartitionByMod.java
PartitionByMonth.java
PartitionByMurmurHash.java
PartitionByPattern.java
PartitionByPrefixPattern.java
PartitionByRangeDateHash.java
PartitionByRangeMod.java
PartitionByString.java
PartitionDirectBySubString.java

数据库表结构

为了验证常用的几种分片规则,此处模拟挂号模块维护挂号患者信息创建如下表

ps:配置完规则后在mycat中执行即可,好处是可以不用登录多个数据节点挨个创建)

create table register
(
   id                   bigint not null comment '主键',
   name                 varchar(200) comment '姓名',
   age                  int comment '年龄',
   gender               varchar(10) comment '性别 01:男,02:女',
   birthday             date comment '出生日期',
   primary key (id)
);

alter table register comment '挂号患者';

1.取 模

此规则为对分片字段求摸运算。上一大章节已经说明,此处不再赘述。

2.分片枚举

通过在配置文件中配置可能的枚举 id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省

份或区县来做保存,而全国省份区县固定的,这类业务使用本条规则

此处模拟挂号患者按照性别枚举,‘男’:存储到第一个数据节点,女存储到第二个数据节点

1.配置schema.xml

在schema.xml中添加table标签<table name="register" dataNode="spt1,spt2" rule="slice_by_enum"></table>

2.修改rule.xml

添加tableRule标签

<tableRule name="slice_by_enum">
    <rule>
        <!-- 分片字段-->
         <columns>gender</columns>
        <!-- 分片算法名 -->
         <algorithm>hash-int</algorithm>
    </rule>
 </tableRule>
<!-- .......此处省略一部分代码 -->
<function name="hash-int"
                class="io.mycat.route.function.PartitionByFileMap">
                <!--分片字段和数据节点对应关系,在txt文档中以属性文件形式存在 -->
                <property name="mapFile">partition-hash-int.txt</property>
                
                <!--分片字段类型,0:数字(默认) 1(非零):表示字符串 -->
                <property name="type">1</property>

                <!-- 默认数据节点,从0开始,如果不设置默认数据节点,当找分片字段的值不在定义规则范围内,则插入会报错-->
                <property name="defaultNode">0</property>
        </function>

上面columns 标识将要分片的表字段,algorithm 分片函数,

其中分片函数配置中:

  • mapFile 标识配置文件名称,
  • type 默认值为 0,0 表示 Integer,非零表示 String,

所有的节点配置都是从 0 开始,及 0 代表节点 1

/**
* defaultNode 默认节点:小于 0 表示不设置默认节点,大于等于 0 表示设置默认节点
* 默认节点的作用:枚举分片时,如果碰到不识别的枚举值,就让它路由到默认节点
* 如果不配置默认节点(defaultNode 值小于 0 表示不配置默认节点),碰到
* 不识别的枚举值就会报错,
* like this:can’t find datanode for sharding column:column_nameval:ffffffff
*/

编辑partition-hash-int.txt

男=0
女=1

重启mycat

./mycat console

登录mycat创建表

create table register
(
   id                   bigint not null comment '主键',
   name                 varchar(200) comment '姓名',
   age                  int comment '年龄',
   gender               varchar(10) comment '性别 01:男,02:女',
   birthday             date comment '出生日期',
   primary key (id)
);

alter table register comment '挂号患者';

在mycat中插入数据库

insert into register (id, name, age, gender, birthday) values('1','aaaa','44','01','1906-06-19');
insert into register (id, name, age, gender, birthday) values('2','bbbb','45','01','1906-06-19');
insert into register (id, name, age, gender, birthday) values('3','cccc','46','02','1906-06-19');
insert into register (id, name, age, gender, birthday) values('4','dddd','47','02','1906-06-19');

分别在mycat中、数据节点spt1、spt2中查询数据

select * from register order by name;
*

3.范围约定

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

start-end=dataNodeIndex,例如0-100:0,101-200:1 其中如果数量级比较大可以使用KM等简写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

1. 验证

此处还是以患者表为例,其中按照年龄分段

  • 0-45:使用数据节点0存储
  • 46-150:使用数据节点1存储
  • 超过范围的使用数据节点0存储

先删除register表中的数据

delete from register;

2.修改schema.xml

重新定义register表的分片规则名"slice_by_range"

<table name="register" dataNode="spt1,spt2" rule="slice_by_range" ></table>

3.修改rule.xml

在rule中添加新的分片规则,名字为上述的“slice_by_range”。

<!-- name: 在schema中表使用的分片规则名称 -->
<tableRule name="slice_by_range">
	<rule>
		<columns>age</columns>
		<!-- 分片规则算法名称-->
		<algorithm>rang-long</algorithm>
	</rule>
</tableRule>

配置name为“rang-long”的function标签,添加defaultNode属性

<function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong">
	<property name="mapFile">autopartition-long.txt</property>
	<!-- 默认数据节点,从0开始,如果不设置默认数据节点,当找分片字段的值不在定义规则范围内,则插入会报错-->
	<property name="defaultNode">0</property>
</function>

4.编辑autopartition-long.txt

配置范围规则

  • 0-45:使用数据节点0存储
  • 46-150:使用数据节点1存储
  • 超过范围的使用数据节点0存储
0-45=0
46-150=1

5.插入数据

分别插入小于45的年龄,小于150的年龄以及大于150年龄的患者信息

insert into register (id, name, age, gender, birthday) values('1','aaaa',42,'01','1906-06-19');
insert into register (id, name, age, gender, birthday) values('2','bbbb',43,'01','1906-06-19');

insert into register (id, name, age, gender, birthday) values('3','cccc',60,'02','1906-06-19');
insert into register (id, name, age, gender, birthday) values('4','dddd',70,'02','1906-06-19');

insert into register (id, name, age, gender, birthday) values('5','eeee',160,'02','1906-06-19');

6.查询

分别在mycat客户端,spt1、spt2上查询数据验证分片结果

select * from register order by age;

*

4.按日期(天、月)分片

1.修改schema.xml

<table name="register" dataNode="spt1,spt2" rule="slice_by_date" ></table>

2.修改rule.xml

添加规则名称为“slice_by_data”的算法

<!-- name: 在schema中表使用的分片规则名称 -->
<tableRule name="slice_by_date">
	<rule>
		<columns>birthday</columns>
		<!-- 分片规则算法名称-->
		<algorithm>sliceByDate</algorithm>
	</rule>
</tableRule>

编辑function

<!-- 按照时间日期分片
		- columns:分片字段,algorithm:分片函数
		- dateFormat :日期格式sBeginDate :开始日期
		- sEndDate:结束日期,则代表数据达到了这个日期的分片后循环从开始分片插入
		- sPartionDay :分区天数,即默认从开始日期算起,分隔 2 天一个分区
	-->
	<function name="sliceByDate" class="io.mycat.route.function.PartitionByDate">
		<property name="dateFormat">yyyy-MM-dd</property>
		<property name="sBeginDate">2021-06-21</property>
		<property name="sEndDate">2021-06-30</property>
		<property name="sPartionDay">2</property> 
	</function>

  • columns:分片字段,algorithm:分片函数
  • dateFormat :日期格式sBeginDate :开始日期
  • sEndDate:结束日期,则代表数据达到了这个日期的分片后循环从开始分片插入
  • sPartionDay :分区天数,即默认从开始日期算起,分隔 2 天一个分区

3.重启mycat,插入数据测试

insert into register (id, name, age, gender, birthday) values('1','aaaa',42,'01','2021-06-21');
insert into register (id, name, age, gender, birthday) values('2','bbbb',43,'01','2021-06-22');
insert into register (id, name, age, gender, birthday) values('3','cccc',60,'02','2021-06-23');
insert into register (id, name, age, gender, birthday) values('4','dddd',70,'02','2021-06-24');
insert into register (id, name, age, gender, birthday) values('5','eeee',40,'02','2021-06-25');

数据节点1:

MySQL [boss]> select * from register;
+----+------+------+--------+------------+
| id | name | age  | gender | birthday   |
+----+------+------+--------+------------+
|  1 | aaaa |   42 | 01     | 2021-06-21 |
|  3 | cccc |   60 | 02     | 2021-06-23 |
|  5 | eeee |   40 | 02     | 2021-06-25 |
+----+------+------+--------+------------+
3 rows in set (0.01 sec)

MySQL [boss]> 

数据节点2:

MySQL [boss]> select * from register;
+----+------+------+--------+------------+
| id | name | age  | gender | birthday   |
+----+------+------+--------+------------+
|  2 | bbbb |   43 | 01     | 2021-06-22 |
|  4 | dddd |   70 | 02     | 2021-06-24 |
+----+------+------+--------+------------+
2 rows in set (0.00 sec)

MySQL [boss]> 

Mycat客户端

MySQL [TESTDB]> select * from register;
+----+------+------+--------+------------+
| id | name | age  | gender | birthday   |
+----+------+------+--------+------------+
|  2 | bbbb |   43 | 01     | 2021-06-22 |
|  4 | dddd |   70 | 02     | 2021-06-24 |
|  1 | aaaa |   42 | 01     | 2021-06-21 |
|  3 | cccc |   60 | 02     | 2021-06-23 |
|  5 | eeee |   40 | 02     | 2021-06-25 |
+----+------+------+--------+------------+
5 rows in set (0.09 sec)

MySQL [TESTDB]> 

5. 固定分片 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 分片范围列表

分区长度:默认为最大 2^n=1024 ,即最大支持 1024 分区

约束:

count,length 两个数组的长度必须是一致的。

1024 = sum((count[i]*length[i])). count 和 length 两个向量的点积恒等于 1024

用法例子:

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

//|<———————1024———————————>|

117// |<—-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);

// 下面代码演示分别以 offerId 字段或 memberId 字段根据上述分区策略拆分的分配结果
int DEFAULT_STR_HEAD_LEN = 8; // cobar 默认会配置为此值
long offerId = 12345;
String memberId = "qiushuo";
// 若根据 offerId 分配,partNo1 将等于 0,即按照上述分区策略,offerId 为 12345 时将会被分配
到 partition0 中
int partNo1 = pu.partition(offerId);
// 若根据 memberId 分配,partNo2 将等于 2,即按照上述分区策略,memberId 为 qiushuo 时将会被
分到 partition2 中
int partNo2 = pu.partition(memberId, 0, DEFAULT_STR_HEAD_LEN);

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

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

6.取模范围约束

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

分布。

实现类:

io.mycat.route.function.PartitionByPattern

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

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

实现类:

io.mycat.route.function.PartitionByPrefixPattern

8.应用指定

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

实现类:

io.mycat.route.function.PartitionDirectBySubString

9.截取数字 hash 解析

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

实现类:

io.mycat.route.function.PartitionByString

10.一致性 hash

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

实现类:

io.mycat.route.function.PartitionByMurmurHash

11.按单月小时拆分

此规则是单月内按照小时拆分,最小粒度是小时,可以一天最多 24 个分片,最少 1 个分片,一个月完后下月

从头开始循环。

每个月月尾,需要手工清理数据。

实现类:

io.mycat.route.function.LatestMonthPartion

12.范围求模分片

先进行范围分片计算出分片组,组内再求模

优点可以避免扩容时的数据迁移,又可以一定程度上避免范围分片的热点问题

综合了范围分片和求模分片的优点,分片组内使用求模可以保证组内数据比较均匀,分片组之间是范围分片可以

兼顾范围查询。

最好事先规划好分片的数量,数据扩容时按分片组扩容,则原有分片组的数据不需要迁移。由于分片组内数据比

较均匀,所以分片组内可以避免热点数据问题

实现类:

io.mycat.route.function.PartitionByRangeMod

13. 日期范围 hash 分片

思想与范围求模一致,当由于日期在取模会有数据集中问题,所以改成 hash 方法。

先根据日期分组,再根据时间 hash 使得短期内数据分布的更均匀

优点可以避免扩容时的数据迁移,又可以一定程度上避免范围分片的热点问题

要求日期格式尽量精确些,不然达不到局部均匀的目的

实现类:

io.mycat.route.function.PartitionByRangeDateHash

14.冷热数据分片

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

实现类:

io.mycat.route.function.PartitionByHotDate

关注“指尖架构师”公众号,获取更多资讯
*

*

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: