一、何为数据切分
在使用Mycat之前,需要明白一个概念,即什么是数据切分。
对于这个问题,简单的来说,就是指通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)上面,以达到分散单台设备负载的效果。
那么从切分层面来讲,切分分为两种形式,垂直切分和水平切分。
1、垂直切分
在实际实施中,垂直切分通常是以功能模块进行切分。比如我们有一个系统,包含用户系统、订单系统、支付系统等诸多子系统,如果是一个小型的项目,它的功能模块自然是共用一个数据库,但是当项目上为了满足生产需要,就会涉及到拆分,拆分成不同的模块,而每个模块是拥有自己的数据库,那么这就带来的数据的切分。拆分如图
那么垂直拆分了以后,有以下几个优点:
- 数据库的拆分简单明了,拆分规则明确;
- 应用程序模块清晰明确,整合容易;
- 数据维护方便易行,容易定位;
当然,也会存在以下几个缺点:
- 部分表关联无法在数据库级别完成,需要在程序中完成,存在跨库join的问题,对于这类的表,就需要去做平衡,是数据库让步业务,共用一个数据源,还是分成多个库,业务之间通过接口来做调用;在系统初期,数据量比较少,或者资源有限的情况下,会选择共用数据源,但是当数据发展到了一定的规模,负载很大的情况,就需要必须去做分割。
- 对于访问极其频繁且数据量超大的表仍然存在性能瓶颈,不一定能满足要求;
- 事务处理相对更为复杂;
- 切分达到一定程度之后,扩展性会遇到限制;
- 过多切分可能会带来系统过渡复杂而难以维护。
2、水平切分
相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中。
(1)优点
- 表关联基本能够在数据库端全部完成;
- 不会存在某些超大型数据量和高负载的表遇到瓶颈的问题;
- 应用程序端整体架构改动相对较少;
- 事务处理相对简单;
- 只要切分规则能够定义好,基本上较难遇到扩展性限制;
(2)缺点
- 切分规则相对更为复杂,很难抽象出一个能够满足整个数据库的切分规则;
- 后期数据的维护难度有所增加,人为手工定位数据更困难;
- 应用系统各模块耦合度较高,可能会对后面数据的迁移拆分造成一定的困难。
- 跨节点合并排序分页问题。
- 多数据源管理问题。
3、几种典型的分片规则
在实际使用,不可能所有分片规则都会用到,需要根据来决定,采用什么的分片规则。
那么一般有以下几种比较类似的典型:
- 按照用户ID求模,将数据分散到不同的数据库,具有相同数据用户的数据都被分散到一个库中。
- 按照日期,将不同月甚至日的数据分散到不同的库中。
- 按照某个特定的字段求摸,或者根据特定范围段分散到不同的库中。
在实际拆分中,我们肯定不能瞎拆分,是需要遵循一定的规则,主要从以下几个方面考虑:
- 能不切分尽量不要切分。
- 如果要切分一定要选择合适的切分规则,提前规划好。
- 数据切分尽量通过数据冗余或表分组(Table Group)来降低跨库Join的可能。
- 由于数据库中间件对数据Join实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表Join。
综上所述,无论是水平拆分,还是垂直拆分,都会带来几个比较核心的问题,也是比较令人头疼的问题。
- 引入分布式事务的问题。
- 跨节点 Join 的问题。
- 跨节点合并排序分页问题。
二、实施
前面讲到了如何拆分以及折分会带来哪些问题。对于数据库拆分,目前有比较多的方案,比如Mycat、ShardingSphere等数据库中间件。
那么本节仅以Mycat进行说明。
1、Mycat是什么
Mycat是一个数据库代理,它本身不存储数据,只做数据的路由。截止目前,Mycat支持的数据库比较多,例如MySQL、SQL Server、Oracle、DB2、PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储。
在Mycat的原理中最重要的一个动词是“拦截”,它拦截了用户发送过来的SQL语句,首先对SQL语句做了一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。
2、Mycat实践
(1)安装
至于mycat的下载安装,可以自行借助强大的搜索引擎的力量。我这里使用的是Mycat的源码。
(2)启动
将下载好的源码,导入到idea开发工具中,配置VM参数,就可以跑起来。
使用navicat连接,效果如图
(3)关键概念
逻辑库(schema) ——存在在mycat里面的虚拟库 逻辑表(table) 存在在mycat里面的虚拟表 。
分片表 ——是指那些原有的很大数据的表,需要切分到多个数据库的表,这样,每个分片都有一部分数据,所有分片构成了完整的数据。
非分片表—— 不需要进行数据切分的表。
ER表——子表的记录与所关联的父表记录存放在同一个数据分片上,即子表依赖于父表,通过表分组(Table Group)保证数据Join不会跨库操作。表分组(Table Group)是解决跨分片数据join的一种很好的思路,也是数据切分规划的重要一条规则。
全局表——例如字典表,每一个数据分片节点上有保存了一份字典表数据 数据冗余是解决跨分片数据join的一种很好的思路,也是数据切分规划的另外一条重要规则。
分片节点(dataNode) ——数据切分后,一个大表被分到不同的分片数据库上面,每个表分片所在的数据库就是分片节点。
节点主机(dataHost) ——数据切分后,每个分片节点(dataNode)不一定都会独占一台机器,同一机器上面可以有多个分片数据库,这样一个或多个分片节点(dataNode)所在的机器就是节点主机(dataHost),为了规避单节点主机并发数限制,尽量将读写压力高的分片节点(dataNode)均衡的放在不同的节点主机(dataHost)。
分片规则(rule)——前面讲了数据切分,一个大表被分成若干个分片表,就需要一定的规则,这样按照某种业务规则把数据分到某个分片的规则就是分片规则,数据切分选择合适的分片规则非常重要,将极大的避免后续数据处理的难度。
全局序列号(sequence) ——数据切分后,原有的关系数据库中的主键约束在分布式条件下将无法使用,因此需要引入外部机制保证数据唯一性标识,这种保证全局性的数据唯一标识的机制就是全局序列号(sequence)。
(4)全局序列号
- 文件方式获得序列号
进入到resources目录下,找到“sequence_conf.properties”文件,做如下配置
GLOBAL_SEQ.HISIDS=
GLOBAL_SEQ.MINID=1001
GLOBAL_SEQ.MAXID=1000000000
GLOBAL_SEQ.CURID=1000
其中HISIDS表示使用过的历史分段(一般无特殊需要可不配置),MINID表示最小ID值,MAXID表示最大ID值,CURID表示当前ID值。GLOBAL_SEQ你可以自己定义。
然后在server.xml文件中,做如下配置
<system><property name="sequnceHandlerType">0</property></system>
注:sequnceHandlerType需要配置为0,表示使用本地文件方式。
使用示例: insert into table1(id,name) values('next value for MYCATSEQ_ORDER','test');
效果如图:
缺点:当MyCAT重新发布后,配置文件中的sequence会恢复到初始值。
优点:本地加载,读取速度较快。
- 基于zookeeper生成全局序列
进入到resources目录下,找到“myid.properties”文件,做如下配置
loadZk=true # 注意,如是要不使用zk时,这个值必须设置为false
zkURL=192.168.1.105:2181
clusterId=01
myid=mycat_fz_01
clusterSize=1
clusterNodes=mycat_fz_01
找到“sequence_distributed_conf.properties”文件,做如下配置
#从zk中获取实例id
INSTANCEID=ZK
#集群id
CLUSTERID=01
然后在server.xml文件中,做如下配置
<system><property name="sequnceHandlerType">3</property></system>
效果如下图
- 基于时间戳生成全局序列
进入到resources目录下,找到“sequence_time_conf.properties”文件,做如下配置
# 0-31为整数,每一个mycat节点的这两个配置值都不一样
WORKID=01 # 机器的id
DATAACENTERID=01 # 机房的id
单从这个配置上看,它是借助了雪花算法的思想。
然后在server.xml文件中,做如下配置
<property name="sequenceHandlerType">2</property>
效果如下图
(4)分片规则
首先需要说明的配置文件
schema.xml——配置逻辑表、逻辑库与真实库的映射关系,以及分片采用的规则。内容参考
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--逻辑库,跟数据库的database的概念相同-->
<!--schema上面的dataNode是逻辑库下面的表的默认数据节点
sqlMaxLimit:效果类似于sql语句里面加上limit,不过如果schema为非分片库则该属性不生效
-->
<schema name="enjoyDB" checkSQLschema="true" dataNode="dn105" sqlMaxLimit="100">
<!--mycat中的逻辑表-->
<!--
name:逻辑表的名称,名称必须唯一
dataNode:值必须跟dataNode标签中的name对应,如果值过多可以用 dataNode="dn$0-99,cn$100-199"
rule:分片规则配置,定义在rule.xml中,必须与tableRule中的name对应
ruleRequired:该属性用于指定表是否绑定分片规则,如果配置为true,但没有配置具体rule的话 ,程序会报错
primaryKey:该逻辑表对应真实表的主键,例如:分片的规则是使用非主键进行分片的,那么在使用主键查询的时候,就会发送查询语句到所有配置的DN上,如果使用该属性配置真实表的主键。难么MyCat会缓存主键与具体DN的信息,那么再次使用非主键进行查询的时候就不会进行广播式的查询,就会直接发送语句给具体的DN,但是尽管配置该属性,如果缓存并没有命中的话,还是会发送语句给具体的DN,来获得数据
type:全局表:global 每一个dn都会保存一份全局表,普通表:不指定该值为globla的所有表
autoIncrement:autoIncrement=“true”,默认是禁用的
needAddLimit:默认是true
-->
<!--注意:这里并没有指定分表,所以做表操作的时候,就是操作的真实的表本身-->
<!-- 另说明:通过mycat中打印的日志中,可以看出,节点0对应的就是dn105, 1对应的是dn110-->
<table dataNode="dn105,dn110" primaryKey="orderId" name="t_order_sharing_by_intfile" rule="sharding-by-intfile"></table>
</schema>
<dataNode name="dn105" database="consult_rule" dataHost="host105"/>
<dataNode name="dn110" database="consult_rule" dataHost="host110"/>
<!--
maxCon:指定每个读写实例连接池的最大连接。也就是说,标签内嵌套的writeHost、readHost标签都会使用这个属性的值来实例化出连接池的最大连接数
minCon:指定每个读写实例连接池的最小连接,初始化连接池的大小
balance:
1. balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的writeHost上。
2. balance="1",全部的readHost与stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且M1与 M2互为主备),正常情况下,M2,S1,S2都参与select语句的负载均衡。
3. balance="2",所有读操作都随机的在writeHost、readhost上分发。
4. balance="3",所有读请求随机的分发到writeHost对应的readhost执行,writerHost不负担读压力,注意balance=3只在1.4及其以后版本有,1.3没有。
writeType:
负载均衡类型,目前的取值有3种:
1. writeType="0", 所有写操作发送到配置的第一个writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后以切换后的为准,切换记录在配置文件中:dnindex.properties .
2. writeType="1",所有写操作都随机的发送到配置的writeHost。
3. writeType="2",没实现
switchType:
-1 表示不自动切换
1 默认值,自动切换
2 基于MySQL主从同步的状态决定是否切换
心跳语句为 show slave status
3 基于MySQL galary cluster的切换机制(适合集群)(1.4.1)
心跳语句为 show status like ‘wsrep%’
-->
<dataHost name="host105" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<!--heartbeat标签
MYSQL可以使用select user()
Oracle可以使用select 1 from dual
-->
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<!-- can have multi write hosts -->
<!--
如果writeHost指定的后端数据库宕机,那么这个writeHost绑定的所有readHost都将不可用。
另一方面,由于这个writeHost宕机系统会自动的检测到,并切换到备用的writeHost上去
-->
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.105:3400" user="root" password="123456">
<readHost host="hostM2" url="jdbc:mysql://192.168.1.105:3401" user="root" password="123456" />
</writeHost>
</dataHost>
<dataHost name="host110" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.110:3400" user="root" password="123456">
<readHost host="hostM2" url="jdbc:mysql://192.168.1.110:3401" user="root" password="123456" />
</writeHost>
</dataHost>
</mycat:schema>
server.xml——系统级别的配置。比如可以在这个文件里面配置连接mycat的账号密码等,如下图
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="rule1">
<rule>
<columns>id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<tableRule name="gd-hash">
<rule>
<columns>orderId</columns>
<algorithm>gd-hash-func</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-date">
<rule>
<columns>createTime</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-date-day">
<rule>
<columns>createTime</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<tableRule name="rule2">
<rule>
<columns>user_id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<!--
分片枚举
通过在配置文件中配置可能的枚举id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,而全国省份区县固定的
-->
<tableRule name="sharding-by-intfile">
<rule>
<!--根据这个字段进行分片-->
<columns>province</columns>
<algorithm>hash-int</algorithm>
</rule>
</tableRule>
<tableRule name="auto-sharding-long">
<rule>
<columns>orderId</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>
<tableRule name="mod-long">
<rule>
<columns>order_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-murmur">
<rule>
<columns>orderId</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>orderId</columns>
<algorithm>jump-consistent-hash</algorithm>
</rule>
</tableRule>
<tableRule name="jch1">
<rule>
<columns>orderDetailId</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">H:\eclipse-for-xiangxue-2qi-workspace\MycatStudy\Mycat-Server-Mycat-server-1675-release\src\main\resources</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>
<!--type默认值为0,0表示Integer,非零表示String-->
<property name="type">1</property>
<!--defaultNode 默认节点:小于0表示不设置默认节点,大于等于0表示设置默认节点,不能解析的枚举就存到默认节点-->
<property name="defaultNode">0</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">3</property>
</function>
<function name="gd-hash-func" class="io.mycat.route.function.PartitionByLong">
<!--下面的配置,表示,一共分了两个分片段。即1个长度为256,一个长度为768-->
<!--为分片个数列表-->
<property name="partitionCount">1,1</property>
<!--分片对应的长度-->
<property name="partitionLength">256,768</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="partbyday"
class="io.mycat.route.function.PartitionByDate">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sNaturalDay">0</property>
<property name="sBeginDate">2020-05-01</property>
<!--
它的作用是指,如果分片的节点数少于(结束日期-开始日期)/10 个节点的话,那么就在真实的分片节点之前来回的切换。
如果不指定的话,就不会来回切换,而是一直往前走,假如真实的只有两个节点,那么可能算出来的是第3个节点,而节点3是不存在的
-->
<property name="sEndDate">2020-05-20</property>
<!--表示每满10天换个分片区。即2020-05-01到2020-05-10对应0分片段,2020-05-11到2020-05-20对应1分片段-->
<!--在几个指定的dn节点之间来回切换-->
<property name="sPartionDay">10</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">2</property>
</function>
</mycat:rule>
用到的数据库脚本如下
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`orderId` bigint(20) NOT NULL,
`orderName` varchar(255) NOT NULL,
`orderType` varchar(255) CHARACTER SET utf8 NOT NULL,
`createTime` datetime DEFAULT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_detail
-- ----------------------------
DROP TABLE IF EXISTS `t_order_detail`;
CREATE TABLE `t_order_detail` (
`orderDetailId` bigint(20) NOT NULL,
`orderId` bigint(20) NOT NULL,
`orderDetailName` varchar(255) NOT NULL,
`createTime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`orderDetailId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_gd_hash
-- ----------------------------
DROP TABLE IF EXISTS `t_order_gd_hash`;
CREATE TABLE `t_order_gd_hash` (
`orderId` bigint(20) NOT NULL,
`orderName` varchar(255) NOT NULL,
`createTime` datetime DEFAULT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_murmur_hash
-- ----------------------------
DROP TABLE IF EXISTS `t_order_murmur_hash`;
CREATE TABLE `t_order_murmur_hash` (
`orderId` bigint(20) NOT NULL,
`orderName` varchar(255) NOT NULL,
`createTime` datetime DEFAULT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_range
-- ----------------------------
DROP TABLE IF EXISTS `t_order_range`;
CREATE TABLE `t_order_range` (
`orderId` bigint(20) NOT NULL,
`orderName` varchar(255) NOT NULL,
`createTime` datetime DEFAULT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_sharing_by_intfile
-- ----------------------------
DROP TABLE IF EXISTS `t_order_sharing_by_intfile`;
CREATE TABLE `t_order_sharing_by_intfile` (
`orderId` bigint(20) NOT NULL,
`orderName` varchar(255) NOT NULL,
`createTime` datetime DEFAULT NULL,
`province` varchar(255) NOT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_time_day
-- ----------------------------
DROP TABLE IF EXISTS `t_order_time_day`;
CREATE TABLE `t_order_time_day` (
`orderId` bigint(20) NOT NULL,
`orderName` varchar(255) NOT NULL,
`createTime` datetime DEFAULT NULL,
PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- ----------------------------
-- Table structure for t_order_type
-- ----------------------------
DROP TABLE IF EXISTS `t_order_type`;
CREATE TABLE `t_order_type` (
`orderType` varchar(255) NOT NULL,
`orderTypeName` varchar(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`orderType`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
1) 枚举分片
在schema文件中的schema标签中加入如下内容
<table dataNode="dn105,dn110" primaryKey="orderId" name="t_order_sharing_by_intfile" rule="sharding-by-intfile"></table>
其中,rule属性的配置在上面rule.xml文件中有说明。可以参考
然后,进入resources目录下,在“partition-hash-int.txt”中配置如下面的格式所示的内容
hunan=0
hubei=1
beijing=0
shanghai=1
chognqing=1
sichuan=1
xizang=0
其中,0、1是表示在schema文件中table标签中配置的“dataNode”的属性值所对应的下标,即0对应dn105节点,1对应 110节点
这样配置完成后,数据就会遵循这个规则被存在不同的库的表中。
应用场景:通过在配置文件中配置可能的枚举id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省
份或区县来做保存,不同的省份存到不同的数据节点。
使用这种方式,会带来一个问题,由于数据是按地域划分的,那么肯定会出现某些结果上的数据特别多。比如上海的订单总 体来说肯定要比西藏的多,那么上海的数据库节点肯定要添加啊,但是如上面的配置文件里面显示的,一个地域只能对应一个节 点,针对这种情况,有什么好的解决方案呢?无非还是使用mycat,如下图
2) 固定hash分片
这个分片,不是那么直观,它的大概原理——类似于十进制的求模运算,区别在于是二进制的操作,是取id的二进制低10位,
即id二进制&1111111111。 此算法的优点在于如果按照10进制取模运算,在连续插入1-10时候1-10会被分到1-10个分片,增大
了插入的事务控制难度,而此算法根据二进制则可能会分到连续的分片,减少插入事务事务控制难度
以上分为三个分区:0-255,256-511,512-1023 故user_id的低10位二进制&1111111111后根据以上范围落入指定的区域。
例如: 1023的二进制&1111111111运算后为1023,故落入第三个分区 1024的二进制&1111111111运算后为0,故落入第一个
分区 266 的二进制&1111111111运算后为266,故落入第二个分区内。
在schema文件中的schema标签中加入如下内容
<table dataNode="dn105,dn110" primaryKey="orderId" name="t_order_gd_hash" rule="gd-hash"></table>
其中,rule属性的配置在上面rule.xml文件中有说明。可以参考
应用场景:这中分片方式使用的场景不多,如机器性能有高低之分的时候,可以考虑这种方式。
它的优点:减少了分布式事务,数据不会散列。
3) 字段范围分片
在schema文件中的schema标签中加入如下内容
<table dataNode="dn105,dn110" primaryKey="orderId" name="t_order_range" rule="auto-sharding-long"></table>
其中,rule属性的配置在上面rule.xml文件中有说明。可以参考
进入resources目录下,在“autopartition-long.txt”文件中做如下配置
0-1000=0
1001-2000=1
其中,0、1是表示在schema文件中table标签中配置的“dataNode”的属性值所对应的下标,即0对应dn105节点,1对应 110节点。
注意,如果被分片的字段的值不在这个配置里面指定的范围中,则在分片的时候,会直接抛出异常。
应用场景:此分片适用于,提前规划好分片字段某个范围属于哪个分片。
4) 按天分片
在schema文件中的schema标签中加入如下内容
<table dataNode="dn105,dn110" primaryKey="orderId" name="t_order_time_day" rule="sharding-by-date-day"></table>
其中,rule属性的配置在上面rule.xml文件中有说明。可以参考
这种分片方式,在实际使用中,比较常见
应用场景:此规则为按天分片
5) 一致性hash
前面四种分片方式,有一个共同的问题,就是会出现数据的分布不均匀
在schema文件中的schema标签中加入如下内容
<table dataNode="dn105,dn110" primaryKey="orderId" name="t_order_murmur_hash" rule="sharding-by-murmur"></table>
其中,rule属性的配置在上面rule.xml文件中有说明。可以参考
应用场景:数据均匀分布,不出现数据倾斜的情况。
原理如下图
6)全局表
在mycat中把一个表做成全局表,需要满足几个特点:
- 数据量少
- 每一个分片都需要用到
- 往往是系统的字典表
为了避免跨库join,所以在保存数据时候,自动地在每一个分片节点里面都保存了一份
在schema文件中的schema标签中加入如下内容
<table dataNode="dn105,dn110" primaryKey="orderType" name="t_order_type" type="global"></table>
在查询全局表中内容的时候,只会随机在某一个节点上这个全局表中查询。
7)ER分片表
在mycat ER分片表,需要满足几个特点:
- 表和表之间存在主从关系
- 存在外键关联
比如:订单表t_order和订单明细表t_order_detail,这种就需要把存在关联关系的id存储到 同一个分片节点里面。
在schema文件中的schema标签中加入如下内容
<table name="t_order" dataNode="dn105,dn110" primaryKey="orderId" rule="jch">
<childTable name="t_order_detail" primaryKey="orderDetailId" joinKey="orderId" parentKey="orderId"/>
</table>
8)mycat中如何解决跨库join
为了解决跨库join,mycat中利用Share join方式进行跨库JOIN,原理就是解析SQL语句,拆分成单表的SQL语句执行,然后把 各个节点的数据汇集。如下
/*!mycat:catlet=io.mycat.catlets.ShareJoin */select b.* from t_order a join t_order_detail b on a.orderId=b.orderId;
9)如何进行批量插入
在使用mycat后,面对批量插入数据时,其实和普通情况下没有区别。以mycat+mybatis为例,写法如下:
@Insert({"<script>",
//"/*!mycat:catlet=io.mycat.route.sequence.BatchInsertSequence */",
"insert into t_order(order_content) values",
"<foreach collection='list' item='item' index='index' separator=','>",
"(#{item.order_content})",
"</foreach>",
"</script>"})
void addT_OrderByBatch(List<TOrder> orders);
需要注意的是,在Mycat中,分片的键只支持一个
三、命令行监控
mycat有两个端口 8066是数据端口,9066是管理端口。
登录9066管理端口:
mysql -uenjoy -p123456 -P9066
查询所有指令
show @@help;
加载配置文件,无需启动mycat(注意会阻塞mycat)
reload @@config
查看数据库
show @@database;
查看分片节点
show @@datanode;
查找对应的schema下面的dataNode列表
show @@datanode where schema = 逻辑库名称;
报告心跳状态
show @@heartbeat;
获取Mycat的前端连接状态,即应用与mycat的连接
show @@connection;
查看数据源状态,如果配置了主从,或者多主可以切换
show @@datasource;
切换数据源
switch @@datasource name:index;
name:schema中配置的dataHost 中name
index:schema中配置的dataHost 的writeHost index 位标,即按照配置顺序从上到下的一次顺 序,从0开始。
切换数据源时,会将原数据源所有的连接池中连接关闭,并且从新数据源创建新连接,此时mycat服务不可用
dnindex.properties 文件在记录了当前的活跃writer。
注:reload @@config, switch @@datasource name:index , 这两个命令再进行处理时,mycat服务不可用,谨慎处理,
防止正在提交的事务出错
四、Mycat数据扩容
我们知道,当我们使用一个HashMap来存储数据时,它是有初始容量大小的,当存储的数据的总量到达总容量的3/4时,就会触发扩容,那么就免不了再hash的过程,扩容后,原有存在的数据的位置就会发生改变。其实Mycat扩容的原理跟HashMap的扩容的原理差不多,都是对数据进行了再分配。
下面就介绍针对Mycat如何做数据的扩容
1、mycat安装
这个安装的过程,还是很简单的,首先自己先去下载好mycat的安装包,如我这里是下载的版本为Mycat-server-1.6.7.5- release-20200422133810-linux.tar.gz,下载好后,进行解压,解压后,进入mycat的conf目录。
schema.xml文件内容如下所示
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--逻辑库,跟数据库的database的概念相同-->
<!--schema上面的dataNode是逻辑库下面的表的默认数据节点
sqlMaxLimit:效果类似于sql语句里面加上limit,不过如果schema为非分片库则该属性不生效
-->
<schema name="enjoydb" checkSQLschema="true" dataNode="dn105" sqlMaxLimit="100">
<!--mycat中的逻辑表-->
<!--
name:逻辑表的名称,名称必须唯一
dataNode:值必须跟dataNode标签中的name对应,如果值过多可以用 dataNode="dn$0-99,cn$100-199"
rule:分片规则配置,定义在rule.xml中,必须与tableRule中的name对应
ruleRequired:该属性用于指定表是否绑定分片规则,如果配置为true,但没有配置具体rule的话 ,程序会报错
primaryKey:该逻辑表对应真实表的主键,例如:分片的规则是使用非主键进行分片的,那么在使用主键查询的时候,就会发送查询语句到所有配置的DN上,如果使用该属性配置真实表的主键。难么MyCat会缓存主键与具体DN的信息,那么再次使用非主键进行查询的时候就不会进行广播式的查询,就会直接发送语句给具体的DN,但是尽管配置该属性,如果缓存并没有命中的话,还是会发送语句给具体的DN,来获得数据
type:全局表:global 每一个dn都会保存一份全局表,普通表:不指定该值为globla的所有表
autoIncrement:autoIncrement=“true”,默认是禁用的
needAddLimit:默认是true
-->
<!--注意:这里并没有指定分表,所以做表操作的时候,就是操作的真实的表本身-->
<table name="t_order" dataNode="dn105,dn110" primaryKey="orderId" rule="jch">
<childTable name="t_order_detail" primaryKey="orderDetailId" joinKey="orderId" parentKey="orderId"/>
</table>
</schema>
<dataNode name="dn105" database="consult_rule" dataHost="host105"/>
<dataNode name="dn110" database="consult_rule" dataHost="host110"/>
<!--
maxCon:指定每个读写实例连接池的最大连接。也就是说,标签内嵌套的writeHost、readHost标签都会使用这个属性的值来实例化出连接池的最大连接数
minCon:指定每个读写实例连接池的最小连接,初始化连接池的大小
balance:
1. balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的writeHost上。
2. balance="1",全部的readHost与stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且M1与 M2互为主备),正常情况下,M2,S1,S2都参与select语句的负载均衡。
3. balance="2",所有读操作都随机的在writeHost、readhost上分发。
4. balance="3",所有读请求随机的分发到writeHost对应的readhost执行,writerHost不负担读压力,注意balance=3只在1.4及其以后版本有,1.3没有。
writeType:
负载均衡类型,目前的取值有3种:
1. writeType="0", 所有写操作发送到配置的第一个writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后以切换后的为准,切换记录在配置文件中:dnindex.properties .
2. writeType="1",所有写操作都随机的发送到配置的writeHost。
3. writeType="2",没实现
switchType:
-1 表示不自动切换
1 默认值,自动切换
2 基于MySQL主从同步的状态决定是否切换
心跳语句为 show slave status
3 基于MySQL galary cluster的切换机制(适合集群)(1.4.1)
心跳语句为 show status like ‘wsrep%’
-->
<dataHost name="host105" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<!--heartbeat标签
MYSQL可以使用select user()
Oracle可以使用select 1 from dual
-->
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<!-- can have multi write hosts -->
<!--
如果writeHost指定的后端数据库宕机,那么这个writeHost绑定的所有readHost都将不可用。
另一方面,由于这个writeHost宕机系统会自动的检测到,并切换到备用的writeHost上去
-->
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.105:3400" user="root" password="123456">
<readHost host="hostM2" url="jdbc:mysql://192.168.1.105:3401" user="root" password="123456" />
</writeHost>
</dataHost>
<dataHost name="host110" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.110:3400" user="root" password="123456">
<readHost host="hostM2" url="jdbc:mysql://192.168.1.110:3401" user="root" password="123456" />
</writeHost>
</dataHost>
</mycat:schema>
再新建一个名为newSchema.xml的文件,注意一定要是这个名称,内容如下
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--逻辑库,跟数据库的database的概念相同-->
<!--schema上面的dataNode是逻辑库下面的表的默认数据节点
sqlMaxLimit:效果类似于sql语句里面加上limit,不过如果schema为非分片库则该属性不生效
-->
<schema name="enjoydb" checkSQLschema="true" dataNode="dn105" sqlMaxLimit="100">
<!--mycat中的逻辑表-->
<!--
name:逻辑表的名称,名称必须唯一
dataNode:值必须跟dataNode标签中的name对应,如果值过多可以用 dataNode="dn$0-99,cn$100-199"
rule:分片规则配置,定义在rule.xml中,必须与tableRule中的name对应
ruleRequired:该属性用于指定表是否绑定分片规则,如果配置为true,但没有配置具体rule的话 ,程序会报错
primaryKey:该逻辑表对应真实表的主键,例如:分片的规则是使用非主键进行分片的,那么在使用主键查询的时候,就会发送查询语句到所有配置的DN上,如果使用该属性配置真实表的主键。难么MyCat会缓存主键与具体DN的信息,那么再次使用非主键进行查询的时候就不会进行广播式的查询,就会直接发送语句给具体的DN,但是尽管配置该属性,如果缓存并没有命中的话,还是会发送语句给具体的DN,来获得数据
type:全局表:global 每一个dn都会保存一份全局表,普通表:不指定该值为globla的所有表
autoIncrement:autoIncrement=“true”,默认是禁用的
needAddLimit:默认是true
-->
<!--注意:这里并没有指定分表,所以做表操作的时候,就是操作的真实的表本身-->
<table name="t_order" dataNode="dn105,dn110,dn110-1" primaryKey="orderId" rule="jch">
<childTable name="t_order_detail" primaryKey="orderDetailId" joinKey="orderId" parentKey="orderId"/>
</table>
</schema>
<dataNode name="dn105" database="consult_rule" dataHost="host105"/>
<dataNode name="dn110" database="consult_rule" dataHost="host110"/>
<dataNode name="dn110-1" database="consult_rule" dataHost="host110-1"/>
<!--
maxCon:指定每个读写实例连接池的最大连接。也就是说,标签内嵌套的writeHost、readHost标签都会使用这个属性的值来实例化出连接池的最大连接数
minCon:指定每个读写实例连接池的最小连接,初始化连接池的大小
balance:
1. balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的writeHost上。
2. balance="1",全部的readHost与stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且M1与 M2互为主备),正常情况下,M2,S1,S2都参与select语句的负载均衡。
3. balance="2",所有读操作都随机的在writeHost、readhost上分发。
4. balance="3",所有读请求随机的分发到writeHost对应的readhost执行,writerHost不负担读压力,注意balance=3只在1.4及其以后版本有,1.3没有。
writeType:
负载均衡类型,目前的取值有3种:
1. writeType="0", 所有写操作发送到配置的第一个writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后以切换后的为准,切换记录在配置文件中:dnindex.properties .
2. writeType="1",所有写操作都随机的发送到配置的writeHost。
3. writeType="2",没实现
switchType:
-1 表示不自动切换
1 默认值,自动切换
2 基于MySQL主从同步的状态决定是否切换
心跳语句为 show slave status
3 基于MySQL galary cluster的切换机制(适合集群)(1.4.1)
心跳语句为 show status like ‘wsrep%’
-->
<dataHost name="host105" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<!--heartbeat标签
MYSQL可以使用select user()
Oracle可以使用select 1 from dual
-->
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<!-- can have multi write hosts -->
<!--
如果writeHost指定的后端数据库宕机,那么这个writeHost绑定的所有readHost都将不可用。
另一方面,由于这个writeHost宕机系统会自动的检测到,并切换到备用的writeHost上去
-->
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.105:3400" user="root" password="123456">
<readHost host="hostM2" url="jdbc:mysql://192.168.1.105:3401" user="root" password="123456" />
</writeHost>
</dataHost>
<dataHost name="host110" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.110:3400" user="root" password="123456">
<readHost host="hostM2" url="jdbc:mysql://192.168.1.110:3401" user="root" password="123456" />
</writeHost>
</dataHost>
<dataHost name="host110-1" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<connectionInitSql></connectionInitSql>
<writeHost host="hostM1" url="jdbc:mysql://192.168.1.110:3403" user="root" password="123456">
</writeHost>
</dataHost>
</mycat:schema>
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="rule1">
<rule>
<columns>id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<tableRule name="gd-hash">
<rule>
<columns>orderId</columns>
<algorithm>gd-hash-func</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-date">
<rule>
<columns>createTime</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-date-day">
<rule>
<columns>createTime</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<tableRule name="rule2">
<rule>
<columns>user_id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<!--
分片枚举
通过在配置文件中配置可能的枚举id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,而全国省份区县固定的
-->
<tableRule name="sharding-by-intfile">
<rule>
<!--根据这个字段进行分片-->
<columns>province</columns>
<algorithm>hash-int</algorithm>
</rule>
</tableRule>
<tableRule name="auto-sharding-long">
<rule>
<columns>orderId</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>
<tableRule name="mod-long">
<rule>
<columns>order_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-murmur">
<rule>
<columns>orderId</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>orderId</columns>
<algorithm>murmur</algorithm>
</rule>
</tableRule>
<tableRule name="jch1">
<rule>
<columns>orderDetailId</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">H:\eclipse-for-xiangxue-2qi-workspace\MycatStudy\Mycat-Server-Mycat-server-1675-release\src\main\resources</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>
<!--type默认值为0,0表示Integer,非零表示String-->
<property name="type">1</property>
<!--defaultNode 默认节点:小于0表示不设置默认节点,大于等于0表示设置默认节点,不能解析的枚举就存到默认节点-->
<property name="defaultNode">0</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">3</property>
</function>
<function name="gd-hash-func" class="io.mycat.route.function.PartitionByLong">
<!--下面的配置,表示,一共分了两个分片段。即1个长度为256,一个长度为768-->
<!--为分片个数列表-->
<property name="partitionCount">1,1</property>
<!--分片对应的长度-->
<property name="partitionLength">256,768</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="partbyday"
class="io.mycat.route.function.PartitionByDate">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sNaturalDay">0</property>
<property name="sBeginDate">2020-05-01</property>
<!--这个字段从测试结果上看,似乎没有起到什么作用-->
<property name="sEndDate">2020-05-20</property>
<!--表示每满10天换个分片区。即2020-05-01到2020-05-10对应0分片段,2020-05-11到2020-05-20对应1分片段-->
<!--在几个指定的dn节点之间来回切换-->
<property name="sPartionDay">10</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">2</property>
</function>
新建一个名称为newRule.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="rule1">
<rule>
<columns>id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<tableRule name="gd-hash">
<rule>
<columns>orderId</columns>
<algorithm>gd-hash-func</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-date">
<rule>
<columns>createTime</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-date-day">
<rule>
<columns>createTime</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<tableRule name="rule2">
<rule>
<columns>user_id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<!--
分片枚举
通过在配置文件中配置可能的枚举id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,而全国省份区县固定的
-->
<tableRule name="sharding-by-intfile">
<rule>
<!--根据这个字段进行分片-->
<columns>province</columns>
<algorithm>hash-int</algorithm>
</rule>
</tableRule>
<tableRule name="auto-sharding-long">
<rule>
<columns>orderId</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>
<tableRule name="mod-long">
<rule>
<columns>order_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<tableRule name="sharding-by-murmur">
<rule>
<columns>orderId</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>orderId</columns>
<algorithm>murmur</algorithm>
</rule>
</tableRule>
<tableRule name="jch1">
<rule>
<columns>orderDetailId</columns>
<algorithm>murmur</algorithm>
</rule>
</tableRule>
<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><!-- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍 -->
<!-- <property name="weightMapFile">weightMapFile</property> 节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,否则以1代替 -->
<property name="bucketMapPath">/usr/soft/mycat/conf</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>
<!--type默认值为0,0表示Integer,非零表示String-->
<property name="type">1</property>
<!--defaultNode 默认节点:小于0表示不设置默认节点,大于等于0表示设置默认节点,不能解析的枚举就存到默认节点-->
<property name="defaultNode">0</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">3</property>
</function>
<function name="gd-hash-func" class="io.mycat.route.function.PartitionByLong">
<!--下面的配置,表示,一共分了两个分片段。即1个长度为256,一个长度为768-->
<!--为分片个数列表-->
<property name="partitionCount">1,1</property>
<!--分片对应的长度-->
<property name="partitionLength">256,768</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="partbyday"
class="io.mycat.route.function.PartitionByDate">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sNaturalDay">0</property>
<property name="sBeginDate">2020-05-01</property>
<!--这个字段从测试结果上看,似乎没有起到什么作用-->
<property name="sEndDate">2020-05-20</property>
<!--表示每满10天换个分片区。即2020-05-01到2020-05-10对应0分片段,2020-05-11到2020-05-20对应1分片段-->
<!--在几个指定的dn节点之间来回切换-->
<property name="sPartionDay">10</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">2</property>
</function>
</mycat:rule>
在server.xml文件,加入以下内容
<user name="enjoy">
<property name="password">123456</property>
<property name="schemas">enjoydb</property>
</user>
在migrateTables.properties文件中加入以下内容
## 要进行数据迁移的表
enjoydb=t_order,t_order_detail
这里说到的表的脚本还是用的上面提到的数据库脚本
修改dataMigrate.sh文件。该文件类型是dos的,需要修改为unix类型 查看.sh文件类型:
在vim编辑模式下,输入命令
:set ff
:set ff=unix
然后,在这个文件中加入一行内容,如下
RUN_CMD="$RUN_CMD -mysqlBin=/usr/bin"
复制mysql数据的mysql和mysqldump两个执行脚本到/usr/bin目录下。此处一定不要少。
上面这些准备工作做完成之后,需要在mycat目录下新建一个logs目录,并在logs目录下新建一个mycat.pid文件。
接下来,进入mycat的bin目录,启动mycat,执行脚本
./mycat start
然后执行 ./dataMigrate.sh 。执行后,会出现下面这个现象,如图
这里,需要安装这个软件
yum install libatomic.so.1
如果安装完成后,还没有解决,则可以下一个Mysql安装包解压,然后,把dataMigrate.sh文件中的RUN_CMD="$RUN_CMD -mysqlBin的值改为你刚解压的mysql的bin目录即可。
执行成功的效果,如下
需要注意事项:
- jdk不能用openJdk,需要自己安装,配置环境变量
- 之前的老的schema.xml和rule.xml老配置不能动
- 新增迁移后的节点新配置newSchema.xml和newRule.xml
- mycat的bug。在migrateTables.properties中逻辑库的名称不能既有大些又有小写,如:myDB,必须全小写或全大写。
- 迁移类:DataMigrator
当迁移完成后,需要把原来的schema.xml和rule.xml两个文件删除掉,然后把newSchema.xml文件名改成schema.xml,把newRule.xml文件名改成rule.xml,重新启动mycat,这样的话,mycat就会以迁移后的规则,进行数据的分片与路由。
mycat在对mysql中的数据进行迁移,目的地可以是oracle、sqlserver、postgresql、mongo等数据库。如果感觉兴趣的话,可以自己去尝试,这里就不做说明了。
五、mycat控制台
1、安装
下载mycat-web,我这里下载的是Mycat-web-1.0-SNAPSHOT-20170102153329-linux.tar.gz版本。
下载完成后解压,进入mycat-web/WEB-INF/classes目录,编辑mycat.properties
show.period=3000000
zookeeper=192.168.1.105:2181
mycat_warn_mail= [{"cc"\:"sohudo@mycat.io","index"\:1,"mangerPort"\:"465","smtpHost"\:"smtp.139.com","smtpPassword"\:"123456","smtpProtocol"\:"smtp","smtpUser"\:"agile_
louie@139.com","to"\:"9183838@qq.com"}]
##sql\u4E0A\u7EBF\u76F8\u5173\u914D\u7F6E
sqlonline.server=192.168.1.105
sqlonline.user=enjoy
sqlonline.passwd=123456
中zookeeper的地址要指定,sqlonline等相关属性是你连接mycat用的。
然后,进入mycat-web目录下,使用jar命令启动
java -jar start.jar
启动完成后,访问http://192.168.1.105:8082/mycat/,即可看到页面,如图
六、Mycat分布式事务
一说到分布式事务,就是一个很大的话题,在Mycat中,是采用XA协议来做的分布式事务。
至于,XA协议可以参考https://blog.csdn.net/weixin_32822759/article/details/106585407,我对XA协议的一点认识。
正是由于Mycat已经基于XA协议实现了分布事务,所以我们在使用Mycat作数据源,使用事务时,还是和普通本地事务一样,直接使用@Transactional即可。
那么在这里,思考一下,为什么只需要使用@Transaction即可呢?
其实原因很简单,因为在使用mycat的情况下,应用程序实际连接是Mycat,而非数据库。而在spring的事务源码中,有这样一句代码 con.setAutoCommit(false);如下图
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
//如果没有数据库连接
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//从连接池里面获取连接
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
//把连接包装成ConnectionHolder,然后设置到事务对象中
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
//从数据库连接中获取隔离级别
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
//关闭连接的自动提交,其实这步就是开启了事务
con.setAutoCommit(false);
}
//设置只读事务 从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!
//设置只读事务就是告诉数据库,我这个事务内没有新增,修改,删除操作只有查询操作,不需要数据库锁等操作,减少数据库压力
prepareTransactionalConnection(con, definition);
//自己提交关闭了,就说明已经开启事务了,事务是活动的
txObject.getConnectionHolder().setTransactionActive(true);
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
//如果是新创建的事务,则建立当前线程和数据库连接的关系
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
这里的连接此时对应的是Mycat的连接,当执行这一条语句后,Mycat就会向它所管理的一些数据库节点发送关闭自动提交的指令。 然后,mycat再向数据发送相关的XA语句了,从而完成事务的操作。在这里Mycat就充当了TM的角色了
这里需要指出的是,在mycat中是采用XA弱事务的。也就是说,Mycat中并不会像atomikos那样会写prepare日志,因为在Mycat看来,它认为如果第一阶段的都成功了,那么第二阶段出问题的可能性是很低的。
七、其它补充点
在使用mycat的过程中,如果遇到了表关联查询的话,则需要注意,
- 目前mycat是不支持跨库三张以上的表的关联查询。而mycat在跨库查询时,查询速度本身就很慢。
- 在mycat进行扩容的时候,最好是将mycat停止掉,再运行dataMigrate.sh。但是如果不想停止,也是可以做到的,就是使用前面提到的,使用锁表的方式,待数据迁移完成后,再将表解锁。这样做的目的,就是为了,在迁移的过程中,避免会数据更新到库中。