功能介绍
Mycat是什么?从定义和分类来看,它是一个开源的分布式数据库系统,是一个实现了MySQL协议的的Server,前端用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原生(Native)协议与多个MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里。
Mycat发展到目前的版本,已经不是一个单纯的MySQL代理了,它的后端可以支持MySQL、SQL Server、Oracle、DB2、
PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储,未来还会支持更多类型的存储。而在最终用户看
来,无论是那种存储方式,在Mycat里,都是一个传统的数据库表,支持标准的SQL语句进行数据的操作,这样一来,对前端业
务系统来说,可以大幅降低开发难度,提升开发速度,在测试阶段,可以将一个表定义为任何一种Mycat支持的存储方式,比如
MySQL的MyASIM表、内存表、或者MongoDB、LevelDB以及号称是世界上最快的内存数据库MemSQL上。试想一下,用户表
存放在MemSQL上,大量读频率远超过写频率的数据如订单的快照数据存放于InnoDB中,一些日志数据存放于MongoDB中,
而且还能把Oracle的表跟MySQL的表做关联查询,你是否有一种不能呼吸的感觉?而未来,还能通过Mycat自动将一些计算分析
后的数据灌入到Hadoop中,并能用Mycat+Storm/Spark Stream引擎做大规模数据分析,看到这里,你大概明白了,Mycat是
什么?Mycat就是BigSQL,Big Data On SQL Database。
对于软件工程师来说,可以这么理解Mycat:
Mycat就是一个近似等于MySQL的数据库服务器,你可以用连接MySQL的方式去连接Mycat(除了端口不同,默认的Mycat端口是8066而非MySQL的3306,因此需要在连接字符串上增加端口信息),大多数情况下,可以用你熟悉的对象映射框架使用
Mycat,但建议对于分片表,尽量使用基础的SQL语句,因为这样能达到最佳性能,特别是几千万甚至几百亿条记录的情况下。
对于架构师来说,可以这么理解Mycat:
Mycat是一个强大的数据库中间件,不仅仅可以用作读写分离、以及分表分库、容灾备份,而且可以用于多租户应用开发、云平台基础设施、让你的架构具备很强的适应性和灵活性,借助于即将发布的Mycat智能优化模块,系统的数据访问瓶颈和热点一目了然,根据这些统计分析数据,你可以自动或手工调整后端存储,将不同的表映射到不同存储引擎上,而整个应用的代码一行也不用改变。
当前是个大数据的时代,但究竟怎样规模的数据适合数据库系统呢?对此,国外有一个数据库领域的权威人士说了一个结论:千亿以下的数据规模仍然是数据库领域的专长,而Hadoop等这种系统,更适合的是千亿以上的规模。所以,Mycat适合1000亿条以下的单表规模,如果你的数据超过了这个规模,请投靠Mycat Plus吧!
工作原理
MyCat的原理最重要的一个动词是“”拦截“”,它拦截了用户发送过来的SQL语句,首先对SQL语句做了一些特定的分析:如分片分析,路由分析,读写分离分析,缓存分析等,然后将此SQL发往后端的真实的数据库,并将返回的结果做适当的处理,最终再返回给用户。
应用场景
Mycat发展到现在,适用的场景已经很丰富,而且不断有新用户给出新的创新性的方案,以下是几个典型的应用场景:
- 单纯的读写分离,此时配置最为简单,支持读写分离,主从切换
- 分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片
- 多租户应用,每个应用一个库,但应用程序只连接Mycat,从而不改造程序本身,实现多租户化
- 报表系统,借助于Mycat的分表能力,处理大规模报表的统计
- 替代Hbase,分析大数据
- 作为海量数据实时查询的一种简单有效方案,比如100亿条频繁查询的记录需要在3秒内查询出来结果,除了基于主键的查
- 询,还可能存在范围查询或其他属性查询,此时Mycat可能是最简单有效的选择
不适合的应用场景
- 设计使用Mycat时有非分片字段查询,请慎重使用Mycat,可以考虑放弃!
- 设计使用Mycat时有分页排序,请慎重使用Mycat,可以考虑放弃!
- 设计使用Mycat时如果要进行表JOIN操作,要确保两个表的关联字段具有相同的数据分布,否则请慎重使用Mycat,可以考虑放弃!
- 设计使用Mycat时如果有分布式事务,得先看是否得保证事务得强一致性,否则请慎重使用Mycat,可以考虑放弃!
需要注意: 在生产环境中, Mycat节点最好使用双节点, 即双机热备环境, 防止Mycat这一层出现单点故障. 可以使用的高可用集群方式有: Keepalived+Mycat+Mysql, Keepalived+LVS+Mycat+Mysql, Keepalived+Haproxy+Mycat+Mysql。本案例采用的一主一从模式的两个mysql实例,并且针对单一的数据库名进行测试;大多数mycat使用场景都是在多主多从模式并针对多个库进行的
。
实现读写分离
搭建mysql主从复制环境
参考https://blog.csdn.net/qq_24313635/article/details/105611675
安装配置MyCat
1、下载Mycat-server-1.6.7.1-release-20190627191042-linux.tar.gz,解压缩文件夹拷贝到/usr/local/目录下
官网上对于这MyCat配置文件的描述是非常详细的,MyCAT 配置主要涉及三个 XML配置文件:
server.xml
:MyCat框架的系统参数/用户参数配置文件schema.xml
: MyCat框架的逻辑库表与分片的配置文件rule.xml
:MyCat框架的逻辑库表分片规则的配置文件
2、修改server.xml
为了和mysql用户做区分,我们将mycat的用户名改为mycat
<?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:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
<system>
<property name="nonePasswordLogin">0</property> <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
<property name="useHandshakeV10">1</property>
<property name="useSqlStat">0</property> <!-- 1为开启实时统计、0为关闭 -->
<property name="useGlobleTableCheck">0</property> <!-- 1为开启全加班一致性检测、0为关闭 -->
<property name="sequnceHandlerType">1</property>
<!--<property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>-->
<!--必须带有MYCATSEQ_或者 mycatseq_进入序列匹配流程 注意MYCATSEQ_有空格的情况-->
<property name="sequnceHandlerPattern">(?:(\s*next\s+value\s+for\s*MYCATSEQ_(\w+))(,|\)|\s)*)+</property>
<property name="subqueryRelationshipCheck">false</property> <!-- 子查询中存在关联查询的情况下,检查关联字段中是否有分片字段 .默认 false -->
<!-- <property name="useCompression">1</property>--> <!--1为开启mysql压缩协议-->
<!-- <property name="fakeMySQLVersion">5.6.20</property>--> <!--设置模拟的MySQL版本号-->
<!-- <property name="processorBufferChunk">40960</property> -->
<!--
<property name="processors">1</property>
<property name="processorExecutor">32</property>
-->
<!--默认为type 0: DirectByteBufferPool | type 1 ByteBufferArena | type 2 NettyBufferPool -->
<property name="processorBufferPoolType">0</property>
<!--默认是65535 64K 用于sql解析时最大文本长度 -->
<!--<property name="maxStringLiteralLength">65535</property>-->
<!--<property name="sequnceHandlerType">0</property>-->
<!--<property name="backSocketNoDelay">1</property>-->
<!--<property name="frontSocketNoDelay">1</property>-->
<!--<property name="processorExecutor">16</property>-->
<!--
<property name="serverPort">8066</property> <property name="managerPort">9066</property>
<property name="idleTimeout">300000</property> <property name="bindIp">0.0.0.0</property>
<property name="frontWriteQueueSize">4096</property> <property name="processors">32</property> -->
<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>
<!--
off heap for merge/order/group/limit 1开启 0关闭
-->
<property name="useOffHeapForMerge">0</property>
<!--
单位为m
-->
<property name="memoryPageSize">64k</property>
<!--
单位为k
-->
<property name="spillsFileBufferSize">1k</property>
<property name="useStreamOutput">0</property>
<!--
单位为m
-->
<property name="systemReserveMemorySize">384m</property>
<!--是否采用zookeeper协调切换 -->
<property name="useZKSwitch">false</property>
<!-- XA Recovery Log日志路径 -->
<!--<property name="XARecoveryLogBaseDir">./</property>-->
<!-- XA Recovery Log日志名称 -->
<!--<property name="XARecoveryLogBaseName">tmlog</property>-->
<!--如果为 true的话 严格遵守隔离级别,不会在仅仅只有select语句的时候在事务中切换连接-->
<property name="strictTxIsolation">false</property>
<property name="useZKSwitch">true</property>
</system>
<!-- 全局SQL防火墙设置 -->
<!--白名单可以使用通配符%或着*-->
<!--例如<host host="127.0.0.*" user="root"/>-->
<!--例如<host host="127.0.*" user="root"/>-->
<!--例如<host host="127.*" user="root"/>-->
<!--例如<host host="1*7.*" user="root"/>-->
<!--这些配置情况下对于127.0.0.1都能以root账户登录-->
<!--
<firewall>
<whitehost>
<host host="1*7.0.0.*" user="root"/>
</whitehost>
<blacklist check="false">
</blacklist>
</firewall>
-->
<user name="mycat" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">MYCATDB</property>
<!-- 表级 DML 权限设置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>
<user name="user">
<property name="password">user</property>
<property name="schemas">MYCATDB</property>
<property name="readOnly">true</property>
</user>
</mycat:server>
3、修改schema.xml
配置读写主机的dataNode
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="MYCATDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1">
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="3"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root" password="123456">
<readHost host="hostS1" url="192.168.190.132:3306" user="root" password="123456" />
</writeHost>
</dataHost>
</mycat:schema>
标签中 balance参数的含义:
balance="0"
:不开启读写分离机制,即读请求仅分发到 writeHost上balance="1"
:读请求随机分发到当前 writeHost对应的 readHost和 standby writeHost上balance="2"
:读请求随机分发到当前 dataHost内所有的 writeHost / readHost上balance="3"
:读请求随机分发到当前 writeHost对应的 readHost上
4、mycat命令
验证
mycat作为数据库中间件要和数据库部署在不同的机器上,所以先验证远程访问的情况
mysql -uroot -p123456 -h 192.168.190.131 -P 3306
mysql -uroot -p123456 -h 192.168.190.132 -P 3306
验证无误后,控制台启动mycat,/usr/local/mycat/bin目录下执行
./mycat console &
mycat启动无误后,登录mycat数据窗口
[root@localhost ~]# mysql -umycat -p123456 -P 8066 -h 192.168.190.131
Warning: Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.29-mycat-1.6.7.1-release-20190627191042 MyCat Server (OpenCloudDB)
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
为了验证mycat读写分离的效果,我们将mycat的日志输出级别改完debug(默认是info级别),在conf
/log4j2
.xml里配置,然后去查询去添加数据在
/logs/mycat
.log日志文件里查看sql被路由到了哪个服务器上面
。
log4j2.xml,level从info改为debug
<asyncRoot level="debug" includeLocation="true">
重启mycat
/usr/local/mycat/bin/mycat restart
打开mycat日志
tail -f /usr/local/mycat/logs/mycat.log
然后我们开始在mycat中执行insert和select语句,观察日志。
读操作,转发到了132机子上面
MySQLConnection [id=14, lastTime=1587382166167, user=root, schema=testdb, old shema=testdb, borrowed=true, fromSlaveDB=true, threadId=40, charset=utf8, txIsolation=3, autocommit=true, attachment=dn1{SELECT * from user_info}, respHandler=SingleNodeHandler [node=dn1{SELECT * from user_info}, packetId=0], host=192.168.190.132, port=3306, statusSync=null, writeQueue=0, modifiedSQLExecuted=false]
to send query cmd:
SELECT * from user_info
in pool
DBHostConfig [hostName=hostS1, url=192.168.190.132:3306]
写操作,转发到了131机子上面
2020-04-20 19:32:29.603 DEBUG [$_NIOREACTOR-0-RW] (io.mycat.backend.mysql.nio.MySQLConnection.synAndDoExecute(MySQLConnection.java:463)) - con need syn ,total syn cmd 1 commands SET names utf8;schema change:false con:MySQLConnection [id=3, lastTime=1587382349602, user=root, schema=testdb, old shema=testdb, borrowed=true, fromSlaveDB=false, threadId=50, charset=utf8, txIsolation=3, autocommit=true, attachment=dn1{insert into user_info (id ,name) values (1,"test")}, respHandler=SingleNodeHandler [node=dn1{insert into user_info (id ,name) values (1,"test")}, packetId=0], host=192.168.190.131, port=3306, statusSync=null, writeQueue=0, modifiedSQLExecuted=true]
查询语句不要加事务,否则读操作会被分发到写服务器上。
执行inser语句时,字段列名不可省略
主备切换
预期实验效果:开启 MyCAT的主备机制后,当主库宕机时,自动切换到备用机进行操作。
关于主备切换,则需要弄清楚 <dataHost />
标签中 switchType
参数的含义:
switchType="-1"
:不自动切换主备数据库switchType="1"
:自动切换主备数据库switchType="2"
:基于MySQL主从复制的状态来决定是否切换,需修改heartbeat语句:show slave status
switchType="3"
:基于Galera(集群多节点复制)的切换机制,需修改heartbeat语句:show status like 'wsrep%'
此处验证一下 Mycat的主备自动切换效果。为此首先我们将 switchType="-1"
设置为 switchType="1"
,并重启 MyCat服务:
实现分库
一个数据库由很多表的构成,每个表对应着不同的业务,垂直切分是指按照业务将表进行分类,分布到不同的数据库上面,这样也就将数据或者说压力分担到不同的库上面,如下图:
系统被切分成了,用户,订单交易,支付几个模块。
一个架构设计较好的应用系统,其总体功能肯定是由很多个功能模块所组成的,而每一个功能模块所需要的数据对应到数据库中就是一个或者多个表。而在架构设计中,各个功能模块相互之间的交互点越统一越少,系统的耦合度就越低,系统各个模块的维护性以及扩展性也就越好。这样的系统,实现数据的垂直切分也就越容易。
但是往往系统之有些表难以做到完全的独立,存在这扩库join的情况,对于这类的表,就需要去做平衡,是数据库让步业务,共用一个数据源,还是分成多个库,业务之间通过接口来做调用。在系统初期,数据量比较少,或者资源有限的情况下,会选择共用数据源,但是当数据发展到了一定的规模,负载很大的情况,就需要必须去做分割。
一般来讲业务存在着复杂join 的场景是难以切分的,往往业务独立的易于切分。如何切分,切分到何种程度是考验技术架构的一个难题。下面来分析下垂直切分的优缺点:
优点:
- 拆分后业务清晰,拆分规则明确
- 系统之间整合或扩展容易
- 数据维护简单
缺点:
- 部分业务表无法join,只能通过接口方式解决,提高了系统复杂度
- 受每种业务不同的限制存在单库性能瓶颈,不易数据扩展跟性能提高
- 事务处理复杂
由于垂直切分是按照业务的分类将表分散到不同的库,所以有些业务表会过于庞大,存在单库读写与存储瓶颈,所以就需要水平拆分来做解决。
我们希望将customer相关的表分布在132从机上,其他的表因为之前mysql主从复制的配置的缘故,所以在131,132机子上都会有。
1、修改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="dn1">
<table name="customer_info" dataNode="dn2"></table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
验证
重启mycat,连接mycat创建表customer_info,order_info,分别插入一些测试数据
create table customer_info(id int,name varchar(50));
insert into customer_info(id,name) values(1,'consumer1');
insert into customer_info(id,name) values(2,'consumer2');
create table order_info(id int,remark varchar(50));
insert into order_info(id,remark) values(1,'order1');
insert into order_info(id,remark) values(1,'order2');
我们分别查看131,132,mycat上面的库表数据。观察发现customer_info表只存在132机子上,其他表在131和132机子上都有。
分在不同机子上面的库表,是不能进行join关联查询的。
mycat:
[SQL]SELECT * from customer_info c left join order_info co on c.id=co.id;
[Err] 1064 - can't find table define in schema ORDER_INFO schema:MYCATDB
而如果在132机子上,这两张表是进行join关联查询:
mysql> SELECT * from customer c left join customer_order co on c.id=co.id;
+------+-----------+------+--------+
| id | name | id | name |
+------+-----------+------+--------+
| 1 | consumer1 | 1 | order1 |
| 1 | consumer1 | 1 | order2 |
| 2 | consumer2 | NULL | NULL |
+------+-----------+------+--------+
3 rows in set (0.00 sec)
所以分库的策略,应该是一些业务相关联的表,划分在同一个库中,因为跨数据库是无法进行连接查询的。
实现分表
相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中,如图:
拆分数据就需要定义分片规则。关系型数据库是行列的二维模型,拆分的第一原则是找到拆分维度。比如:从会员的角度来分析,商户订单交易类系统中查询会员某天某月某个订单,那么就需要按照会员结合日期来拆分,不同的数据按照会员ID做分组,这样所有的数据查询join都会在单库内解决;如果从商户的角度来讲,要查询某个商家某天所有的订单数,就需要按照商户ID 做拆分;但是如果系统既想按会员拆分,又想按商家数据,则会有一定的困难。如何找到合适的分片规则需要综合考虑衡量。几种典型的分片规则包括:
- 按照用户ID求模,将数据分散到不同的数据库,具有相同数据用户的数据都被分散到一个库中
- 按照日期,将不同月甚至日的数据分散到不同的库中
- 按照某个特定的字段求摸,或者根据特定范围段分散到不同的库中
如图,切分原则都是根据业务找到适合的切分规则分散到不同的库,下面用用户ID 求模举例:
既然数据做了拆分有优点也就优缺点。
优点:
- 拆分规则抽象好,join操作基本可以数据库做;
- 不存在单库大数据,高并发的性能瓶颈;
- 应用端改造较少
- 提高了系统的稳定性跟负载能力。
缺点:
- 拆分规则难以抽象
- 分片事务一致性难以解决
- 数据多次扩展难度跟维护量极大
- 跨库join性能较差
前面讲了垂直切分跟水平切分的不同跟优缺点,会发现每种切分方式都有缺点,但共同的特点缺点有:
- 引入分布式事务的问题;
- 跨节点Join的问题;
- 跨节点合并排序分页问题;
- 多数据源管理问题
针对数据源管理,目前主要有两种思路:
- 客户端模式,在每个应用程序模块中配置管理自己需要的一个(或者多个)数据源,直接访问各个数据库,在模块内完成数据的整合;
- 通过中间代理层来统一管理所有的数据源,后端数据库集群对前端应用程序透明;
可能90%以上的人在面对上面这两种解决思路的时候都会倾向于选择第二种,尤其是系统不断变得庞大复杂的时候。确实,这是一个非常正确的选择,虽然短期内需要付出的成本可能会相对更大一些,但是对整个系统的扩展性来说,是非常有帮助的。Mycat 通过数据切分解决传统数据库的缺陷,又有了NoSQL 易于扩展的优点。通过中间代理层规避了多数据源的处理问题,对应用完全透明,同时对数据切分后存在的问题,也做了解决方案。
下面章节就分析,mycat的由来及如何进行数据切分问题。由于数据切分后数据Join 的难度在此也分享一下数据切分的经验:
- 第一原则:能不切分尽量不要切分。
- 第二原则:如果要切分一定要选择合适的切分规则,提前规划好。
- 第三原则:数据切分尽量通过数据冗余或表分组(Table Group)来降低跨库Join 的可能。
- 第四原则:由于数据库中间件对数据Join 实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表Join。
我们新建一个product_info表,希望通过product_type对数据进行取模,水平分表
1、修改schema.xml
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="MYCATDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1">
<table name="customer" dataNode="dn2"></table>
<table name="product_info" dataNode="dn1,dn2" rule="mod_rule"></table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
2、修改rule.xml
新增一个自定义rule
<tableRule name="mod_rule">
<rule>
<columns>product_type</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
因为只有两个数据库,所以将mod-long的的count改为2
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<property name="count">2</property>
</function>
验证
重启mycat,连接mycat创建表product_info,插入一些测试数据
create table product_info(id int,product_type int,remark varchar(50));
insert into product_info(id,product_type ,remark ) values(1,1,'remark');
insert into product_info(id,product_type ,remark ) values(2,2,'remark');
insert into product_info(id,product_type ,remark ) values(3,3,'remark');
insert into product_info(id,product_type ,remark ) values(4,4,'remark');
insert into product_info(id,product_type ,remark ) values(5,5,'remark');
insert into product_info(id,product_type ,remark ) values(6,1,'remark');
insert into product_info(id,product_type ,remark ) values(7,2,'remark');
insert into product_info(id,product_type ,remark ) values(8,3,'remark');
product_info表product_type为偶数的在131机子上面,product_type为奇数的在132机子上面,实现了根据product_type水平的分表
分片的join
MyCat借鉴了NewSQL领域的新秀FoundationDB的设计思路,FoundationDB创新性的提出了Table Group的概念,其将子表的存储位置依赖于主表,并且物理上紧邻存放,因此彻底解决了join效率和性能问题,根据这一思路,提出了ER关系的数据分片策略,指标的记录与所关联的父表记录存放在同一个数据分片上。
我们希望product_info表中对应的product_detail的数据的分片能同product_info中的数据分片保持一致。
修改schema.xml,新增childTable
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="MYCATDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1">
<table name="customer" dataNode="dn2"></table>
<table name="product_info" dataNode="dn1,dn2" rule="mod_rule">
<childTable name="product_detail" primaryKey="id" joinKey="product_id" parentKey="id"></childTable >
</table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
验证
重启mycat,连接mycat创建表product_detail,插入一些测试数据
create table product_detail(id int,product_id int);
insert into product_detail(id,product_id ) values(1,1);
insert into product_detail(id,product_id ) values(2,2);
insert into product_detail(id,product_id ) values(3,3);
insert into product_detail(id,product_id ) values(4,4);
insert into product_detail(id,product_id ) values(5,5);
product_detail表的数据跟着product_info表,通过product_id字段关联来进行水平分片。
全局表
在分片的情况下,当业务表因为规模而进行分片以后,业务表与这些附属的字典表之间的关联,就成了比较棘手的问题,考虑到字典表具有以下几个特性
- 变动不频繁
- 数据量总体变化不大
- 数据规模不大,很少有超过数十万条记录
鉴于此, MyCat定义了一种特殊的表,称之为“全局表”,全局表具有以下特点:
- 全局表的插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性
- 全局表的查询操作,只从一个节点获取
- 全局表可以跟任何一个表进行join操作
如果你的业务中有些数据类似于数据字典,比如配置文件的配置,常用业务的配置或者数据量不大很少变动的表,这些表往往不
是特别大,而且大部分的业务场景都会用到,那么这种表适合于Mycat全局表,无须对数据进行切分,只要在所有的分片上保存一份数据即可,Mycat 在Join操作中,业务表与全局表进行Join聚合会优先选择相同分片内的全局表join,避免跨库Join,在进行
数据插入操作时,mycat将把数据分发到全局表对应的所有分片执行,在进行数据读取时候将会随机获取一个节点读取数据。
目前Mycat没有做全局表的数据一致性检查,后续版本1.4之后可能会提供全局表一致性检查,检查每个分片的数据一致性。
修改schema.xml,添加global表的配置
<?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="dn1">
<table name="customer" dataNode="dn2"></table>
<table name="orders" dataNode="dn1,dn2" rule="mod_rule">
<childTable name="orders_detail" primaryKey="id" joinKey="order_id" parentKey="id"></childTable >
</table>
<table name="dict_order_type" dataNode="dn1,dn2" type="global"></table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
重启mycat,连接mycat创建表dict_order_type,插入一些测试数据
create table dict_order_type(id int,type varchar(50));
insert into dict_order_type(id,type) values(1,'普通');
insert into dict_order_type(id,type) values(2,'团购');
131机子,132机子,mycat上面各自都维护了一份全局表的数据
常用的分片规则
取模分片
此规则对分片字段求模运算。也是水平分表最常用的规则,我们上面order_info表就是采用了此规则。
枚举分片
通过在配置文件中配置可能的枚举id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,
而全国省份区县固定的,这类业务使用本条规则。
我们希望order_ware_info表中字段area_code=35的数据保存在131机子上,area_code=15和其它的数据保存在132机子上。
1、修改schema.xml文件:
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="MYCATDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1">
<table name="customer" dataNode="dn2"></table>
<table name="orders" dataNode="dn1,dn2" rule="mod_rule">
<childTable name="orders_detail" primaryKey="id" joinKey="order_id" parentKey="id"></childTable >
</table>
<table name="dict_order_type" dataNode="dn1,dn2" type="global"></table>
<table name="order_ware_info" dataNode="dn1,dn2" rule="sharding_by_intfile"></table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
2、修改rule.xml文件:
<tableRule name="sharding_by_intfile">
<rule>
<columns>area_code</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">1</property>
<property name="defaultNode">0</property>
</function>
上面columns 标识将要分片的表字段,algorithm 分片函数,其中分片函数配置中,mapFile标识配置文件名称,type默认值为0,0表示Integer,非零表示String,所有的节点配置都是从0开始,及0代表节点1
3、修改partition-hash-int.txt文件:
15=0
35=1
35存在131机子,其它存在132机子
重启mycat,连接mycat创建表order_ware_info,插入一些测试数据
create table order_ware_info(id int,area_code varchar(50));
insert into order_ware_info(id,area_code ) values(1,"35");
insert into order_ware_info(id,area_code ) values(2,"35");
insert into order_ware_info(id,area_code ) values(3,"35");
insert into order_ware_info(id,area_code ) values(4,"15");
insert into order_ware_info(id,area_code ) values(5,"15");
insert into order_ware_info(id,area_code ) values(6,"15");
insert into order_ware_info(id,area_code ) values(7,"1");
insert into order_ware_info(id,area_code ) values(8,"2");
insert into order_ware_info(id,area_code ) values(9,"3");
范围分片
此分片适用于,提前规划好分片字段某个范围属于哪个分片。
我们希望payment_info表中,amount小于100的保存在131机子,大于100的保存在132机子上。
1、修改schema.xml文件:
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="MYCATDB" checkSQLschema="false" sqlMaxLimit="100" dataNode="dn1">
<table name="customer" dataNode="dn2"></table>
<table name="orders" dataNode="dn1,dn2" rule="mod_rule">
<childTable name="orders_detail" primaryKey="id" joinKey="order_id" parentKey="id"></childTable >
</table>
<table name="dict_order_type" dataNode="dn1,dn2" type="global"></table>
<table name="order_ware_info" dataNode="dn1,dn2" rule="sharding_by_intfile"></table>
<table name="payment_info" dataNode="dn1,dn2" rule="auto_sharding_long"></table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
2、修改rule.xml文件:
<tableRule name="auto_sharding_long">
<rule>
<columns>amount</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>
3、修改autopartition-long.txt文件:
0-100=0
101-200=1
重启mycat,连接mycat创建表payment_info,插入一些测试数据
create table payment_info(id int,amount int);
insert into payment_info(id,amount ) values(1,1);
insert into payment_info(id,amount ) values(2,2);
insert into payment_info(id,amount ) values(3,3);
insert into payment_info(id,amount ) values(4,105);
insert into payment_info(id,amount ) values(5,106);
insert into payment_info(id,amount ) values(6,107);
insert into payment_info(id,amount ) values(7,1000);
insert into payment_info(id,amount ) values(8,1001);
日期分片
此规则为按天分片。设定时间格式,范围。
我们希望login_info表中,login_date从2020-01-01开始,每隔2天,分别保存在131机子,132机子上。
1、修改shema.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="dn1">
<table name="customer" dataNode="dn2"></table>
<table name="orders" dataNode="dn1,dn2" rule="mod_rule">
<childTable name="orders_detail" primaryKey="id" joinKey="order_id" parentKey="id"></childTable >
</table>
<table name="dict_order_type" dataNode="dn1,dn2" type="global"></table>
<table name="order_ware_info" dataNode="dn1,dn2" rule="sharding_by_intfile"></table>
<table name="payment_info" dataNode="dn1,dn2" rule="auto_sharding_long"></table>
<table name="login_info" dataNode="dn1,dn2" rule="sharding_by_date"></table>
</schema>
<dataNode name="dn1" dataHost="host1" database="testdb" />
<dataNode name="dn2" dataHost="host2" database="testdb" />
<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="192.168.190.131:3306" user="root"
password="123456">
</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>
<writeHost host="hostM2" url="192.168.190.132:3306" user="root"
password="123456">
</writeHost>
</dataHost>
</mycat:schema>
2、修改rule.xml文件:
<tableRule name="sharding_by_date">
<rule>
<columns>login_date</columns>
<algorithm>shardingByDate</algorithm>
</rule>
</tableRule>
<function name="shardingByDate" class="io.mycat.route.function.PartitionByDate">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sBeginDate">2020-01-01</property>
<property name="sEndDate">2020-01-04</property>
<property name="sPartionDay">2</property>
</function>
spartionDay:分区天数,即默认从开始日期算起,每隔2天一个分区。
重启mycat,连接mycat创建表login_info,插入一些测试数据
create table login_info(login_date date,name varchar(50));
insert into login_info(login_date,name) values("2020-01-01","test");
insert into login_info(login_date,name) values("2020-01-02","test");
insert into login_info(login_date,name) values("2020-01-03","test");
insert into login_info(login_date,name) values("2020-01-04","test");
insert into login_info(login_date,name) values("2020-01-05","test");
insert into login_info(login_date,name) values("2020-01-06","test");
insert into login_info(login_date,name) values("2020-01-07","test");
insert into login_info(login_date,name) values("2020-01-08","test");
insert into login_info(login_date,name) values("2020-01-09","test");
insert into login_info(login_date,name) values("2020-01-10","test");
insert into login_info(login_date,name) values("2020-01-11","test");
insert into login_info(login_date,name) values("2020-01-12","test");
insert into login_info(login_date,name) values("2020-01-13","test");
insert into login_info(login_date,name) values("2020-01-14","test");
insert into login_info(login_date,name) values("2020-01-15","test");
insert into login_info(login_date,name) values("2020-01-16","test");
insert into login_info(login_date,name) values("2020-01-17","test");
insert into login_info(login_date,name) values("2020-01-18","test");
insert into login_info(login_date,name) values("2020-01-19","test");
insert into login_info(login_date,name) values("2020-01-20","test");
insert into login_info(login_date,name) values("2020-01-21","test");
insert into login_info(login_date,name) values("2020-01-22","test");
insert into login_info(login_date,name) values("2020-01-23","test");
insert into login_info(login_date,name) values("2020-01-24","test");
insert into login_info(login_date,name) values("2020-01-25","test");
insert into login_info(login_date,name) values("2020-01-26","test");
insert into login_info(login_date,name) values("2020-01-27","test");
insert into login_info(login_date,name) values("2020-01-28","test");
insert into login_info(login_date,name) values("2020-01-29","test");
insert into login_info(login_date,name) values("2020-01-30","test");
全局序列
利用数据库的一个表来进行计数累加,但是并不是每次生成序列都是读写数据库,这样效率太低。MyCat会预加载一部分号段到MyCat的内存中,这样大部分读写序列都是在内存中完成的。如果内存中的号段用完了,myCat会再向数据库要一次。如果MyCat奔溃了,那么内存中的序列是丢失,重启后,丢失的是当前号段没用完的序列,当时不会因此出现主键重复。下面我们采用数据库自增的方式来生成全局序列。
1、创建序列表和相关函数:
我们在131机子上面执行
DROP TABLE IF EXISTS MYCAT_SEQUENCE;
CREATE TABLE MYCAT_SEQUENCE (
NAME VARCHAR (50) NOT NULL,
current_value INT NOT NULL,
increment INT NOT NULL DEFAULT 100,
PRIMARY KEY (NAME)
) ENGINE = INNODB ;
INSERT INTO MYCAT_SEQUENCE(NAME,current_value,increment) VALUES ('GLOBAL', 100000, 100);
DROP FUNCTION IF EXISTS `mycat_seq_currval`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_currval`(seq_name VARCHAR(50))
RETURNS VARCHAR(64) CHARSET utf8
DETERMINISTIC
BEGIN DECLARE retval VARCHAR(64);
SET retval="-999999999,null";
SELECT CONCAT(CAST(current_value AS CHAR),",",CAST(increment AS CHAR) ) INTO retval
FROM MYCAT_SEQUENCE WHERE NAME = seq_name;
RETURN retval ;
END
;;
DELIMITER ;
DROP FUNCTION IF EXISTS `mycat_seq_nextval`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_nextval`(seq_name VARCHAR(50)) RETURNS VARCHAR(64)
CHARSET utf8
DETERMINISTIC
BEGIN UPDATE MYCAT_SEQUENCE
SET current_value = current_value + increment
WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END
;;
DELIMITER ;
DROP FUNCTION IF EXISTS `mycat_seq_setval`;
DELIMITER ;;
CREATE FUNCTION `mycat_seq_setval`(seq_name VARCHAR(50), VALUE INTEGER)
RETURNS VARCHAR(64) CHARSET utf8
DETERMINISTIC
BEGIN UPDATE MYCAT_SEQUENCE
SET current_value = VALUE
WHERE NAME = seq_name;
RETURN mycat_seq_currval(seq_name);
END
;;
DELIMITER ;
2、修改server.xml文件:
修改全局序列类型为数据库的方式,0,本地文件,1数据库方式,2时间戳方式
<property name="sequnceHandlerType">1</property>
3、sequence_db_conf.properties
ORDERS=dn1
4、向MYCAT_SEQENCE表插入orders表的序列规则,order表的主键从1开始自增,mycat服务器每次从库表获取100天序列缓存。
insert into MYCAT_SEQENCE(NAME,current_value,increment) values("ORDERS",1,100)
重启mycat,连接mycat向orders表插入一些测试数据
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,1,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,2,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,3,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,4,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,5,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,6,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,7,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,8,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,9,"test");
insert into orders(id,customer_id,name) values(next value for MYCATSEQ_ORDERS,10,"test");
插入成功,order表的主键从1开始自增
目前存在的限制
Mycat 目前存在的限制:
- 部分SQL 还不能很好的支持
- 除了分片规则相同、ER分片、全局表、以及SharedJoin,其他表之间的Join问题目前还没有很好的解决,需要自己编写Catlet 来处理。
- 不支持Insertinto中不包括字段名的SQL
- insertintox selectfromy的SQL,若x与y不是相同的分片规则,则不被支持,此时会涉及到跨分片转移。
- 跨分片的事务,目前只是弱XA模式,还没完全实现XA模式。
- 分片的Table,目前不能执行LockTable这样的语句,因为这种语句会随机发到某个节点,也不会全部分片锁定,经常导致死锁问题,此类问题常常出现在sqldump导入导出SQL数据的过程中。
- 目前sql解析器采用Druid,再某些sql例如order,group,sum,count条件下,如果这类操作会出现兼容问题,比如:
这条语句select列的别名与orderby不一致解析器会出现异常,所以在对列加别名时候要注意这类操作异常,特别是由jpa 等类似的框架生成的语句会有兼容问题。
- 开发框架方面,虽然支持Hibernate,但不建议使用Hibernate,而是建议Mybatis以及直接JDBC操作,原因Hibernat 无法控制SQL 的生成,无法做到对查询SQL 的优化,导致大数量下的性能问题。此外,事务方面,建议自己手动控制,查询语句尽量走自动提交事务模式,这样Mycat 的读写分离会被用到,提升性能很明显