MySQL+MyCat 分库分表

MyCat介绍

系统开发中,数据库是非常重要的一个点。除了程序的本身的优化,如:SQL语句优化、代码优化,数据库的处理本身优化也是非常重要的。主从、热备、分表分库等都是系统发展迟早会遇到的技术问题问题。

MyCat是一个开源的分布式数据库系统,是一个实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原生协议与多个MySQL服务器通信,也可以用JDBC协议与大多数主流数据库服务器通信,其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里。

MyCat发展到目前的版本,已经不是一个单纯的MySQL代理了,它的后端可以支持MySQL、SQL Server、Oracle、DB2、PostgreSQL等主流数据库,也支持MongoDB这种新型NoSQL方式的存储,未来还会支持更多类型的存储。而在最终用户看来,无论是那种存储方式,在MyCat里,都是一个传统的数据库表,支持标准的SQL语句进行数据的操作,这样一来,对前端业务系统来说,可以大幅降低开发难度,提升开发速度。

20200420190855

MyCat 是使用 Java 编写的数据库中间件,运行在代码应用和 MySQL 数据库之间的应用。它的前身是阿里开发的数据库中间件corba,实现 MySQL 数据库分库分表集群管理的中间件,曾经出现过重大事故而放弃,被部分人员进行二次开发,形成 Mycat,使用 MyCat 之后,编写的所有的 SQL 语句必须严格遵守 SQL 标准规范.
使用 MyCat 中间件后的结构图如下:

20200420190928

当我们的应用只需要一台数据库服务器的时候我们并不需要Mycat,而如果你需要分库甚至分表,这时候应用要面对很多个数据库的时候,这个时候就需要对数据库层做一个抽象,来管理这些数据库,而最上面的应用只需要面对一个数据库层的抽象或者说数据库中间件就好了,这就是Mycat的核心作用。

所以可以这样理解:数据库是对底层存储文件的抽象,而Mycat是对数据库的抽象。

数据库切分

逻辑上的切分,在物理层面,是使用多库[database],多表[table]实现切分。分为横向和纵向切分;
横向切分:把一个表切分成多个表,相比纵向切分配置麻烦,无法实现表连接查询. 将一张表的字段,分散到若干张表中,将若干表连接到一起,才是当前表的完整数据。
纵向切分:把一个数据库切分成多个数据库,配置方便,只能实现两张表的表连接查询,将一张表中的数据,分散到若干个 database 的同结构表中。多个表的数据的集合是当前表格的数据。

Mycat 中的DB

Mycat 中定义的 database是逻辑上的,但是物理上未必存在。主要是针对纵向切分提供的概念,访问 MyCat,就是将 MyCat 当做 MySQL 使用。

db 数据库是 MyCat 中定义的 database。通过 SQL 访问 MyCat 中的 db 库的时候,对应的是 MySQL 中的 db1,db2,db3 三个库。物理上的 database 是 db1,db2,db3.逻辑上的
database 就是 db。

Mycat 中的表

逻辑意义上的表,一个MaCat的table可以对应物理库上的多个表。

Mycat 默认端口:8066

数据主机:dataHost

物理 MySQL 存放的主机地址,可以使用主机名、IP、域名定义。
数据节点:dataNode 物理的 database 是什么,数据保存的物理节点就是 database。
分片规则:当控制数据的时候,如何访问物理 database 和 table, 就是访问 dataHost 和 dataNode 的算法。
在 Mycat 处理具体的数据 CRUD 的时候,如何访问 dataHost 和 dataNode 的算法.如:哈希算法,crc16 算法等

MyCat实现分库分表读写分离

一、搭建准备

MyCat:192.168.1.105
MySQL master:192.168.1.103
MySQL slave:192.168.1.104

在需要搭建环境的机器上安装jdk,MySQL。这里使用上个例子的节点,关闭MySQL-Proxy功能,在原有的MySQL-Proxy机器上安装MyCat。
两台数据库上设置可供mycat访问的用户密码
mysql> grant all privileges on *.* to 'mycat'@'%' identified by 'mycat' with grant option;
mysql> flush privileges;

二、安装配置MyCat

1、下载Mycat官网:http://www.mycat.io/

下载Mycat-server-1.6.6.1-release-20181031195535-linux.tar.gz到Linux并解压到/usr/local/mycat
bin        mycat命令,启动、重启、停止等
catlet        catlet为Mycat的一个扩展功能
conf        Mycat 配置信息,重点关注
lib        Mycat引用的jar包,Mycat是java开发的
logs        日志文件,包括Mycat启动的日志和运行的日志。
直接运行测试一下:
[root@localhost mycat]# bin/mycat start
Starting Mycat-server...
[root@localhost mycat]# bin/mycat status
Mycat-server is running (20038).
远程登录看一下
[root@localhost mysql-proxy]# mysql -uroot -p123456 -h192.168.1.105 -P8066
mysql> show databases;
+----------+
| DATABASE |
+----------+
| TESTDB   |
+----------+
mysql> use TESTDB;
mysql> show tables;
+------------------+
| Tables in TESTDB |
+------------------+
| company          |
| customer         |
| customer_addr    |
| employee         |
| goods            |
| hotnews          |
| orders           |
| order_items      |
| travelrecord     |
+------------------+

初步测试MyCat没问题,配置文件中配置的逻辑表和库都能查到,但是不能查看数据,因为数据永远在物理库中,并且默认也米有配置对应真实的物理库。

2、配置示例测试

示例、配置分库,比如一个库demo的user表中有20w数据,现在进行分3个库,表结构一样,大概分别存6w左右数据。

第一步,创建好分库,表可以先不用创建。分别为demo1,demo2,demo3。

schema.xml配置如下:需要分库的数据库也放进来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!--逻辑库表创建-->
<schema name="demo" checkSQLschema="false" sqlMaxLimit="100">
<table name="user" dataNode="demo1,demo2,demo3" rule="crc32slot" />
</schema>
<schema name="olddemo" checkSQLschema="false" sqlMaxLimit="100">
<table name="user" dataNode="demo_old"/>
</schema>

<!--逻辑节点,对应物理库-->
<dataNode name="demo1" dataHost="host103" database="demo1" />
<dataNode name="demo2" dataHost="host103" database="demo2" />
<dataNode name="demo3" dataHost="host103" database="demo3" />
<dataNode name="demo_old" dataHost="host103" database="demo" />

<!--物理库配置-->
<dataHost name="host103" 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.1.103:3306" user="mycat" password="mycat">
<readHost host="hostS1" url="192.168.1.104:3306" user="mycat" password="mycat" />
</writeHost>
</dataHost>
</mycat:schema>
rule.xml中修改,因为使用crc32slot分片规则,这个规要配置分片的数据库节点数量
<function name="crc32slot" class="io.mycat.route.function.PartitionByCRC32PreSlot">
<property name="count">3</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
</function>
server.xml配置用户和该用户使用规则
<user name="root" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">demo,olddemo</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>

第二步:创建表,直接在MyCat里创建,都会分别同步到demo123库中

1
2
3
4
5
CREATE TABLE `user` (
`id` int(10) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)

在三个库查看表结构,都ok,slave节点也查看。

mysql> show tables from demo3;
+-----------------+
| Tables_in_demo3 |
+-----------------+
| user            |
+-----------------+

mysql> desc demo1.user;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(10)     | NO   | PRI | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
| _slot | int(11)     | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

发现多了一个_slot字段,这是因为使用了crc32slot分片规则得到的hash值。、

通过写脚本或者其他方式把源数据通过到新库。

读写分离设置

balance=”1”,全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡,简单的说,当双主双从模式(M1->S1,M2->S2,并且 M1 与 M2 互为主备),正常情况下,M2,S1,S2 都参与 select 语句的负载均衡。

balance=”3”,所有读请求随机的分发到 wiriterHost 对应的 readhost 执行,writerHost 不负担读压力,注意 balance=3 只在 1.4 及其以后版本有,1.3 没有。

示例:分表,单裤分表

场景:demo库中t_user表分成两个库t_users1/t_users2

核心配置:

1
2
3
4
5
6
7
8
<schema name="olddemo" checkSQLschema="false" sqlMaxLimit="1000000">
<table name="user" dataNode="demo_old"/>
<table name="t_users" subTables="t_users$1-2" primaryKey="id" dataNode="demo_old" rule="mod-long" />
</schema>
rule:
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<property name="count">2</property>
</function>

在数据库分别创建好t_users1/t_users2

1
2
3
4
5
CREATE TABLE `t_users1` (
`id` int(10) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
)

启动并连接到MyCat:把老表中的数据通过MyCat复制到新表,在、查看数据,可以看出,根据mod-long规则每条数据交替存储到新表。

查询分析可以看出去两个物理表中查询数据:

20200420190612

查询结果是把两个表结果合并起来:

20200420190637

bin/mycat console 启动不成功可以通过这个命令查看原因
logs/wrapper.log   日志中记录的是所有的 mycat 操作. 查看的时候主要看异常信息 caused by 信息    

注:使用分片规则,MyCat启动后会在conf目录中创建一个ruledata下有使用规则信息分配的一个缓存文件,如果修改的分配规则,再次重启MyCat发现运用这个规则的缓存文件在就不会去重新生成,可能就得不到预计结果,所以,在重启前把这个目录对应的文件删除,让MyCat重新生成。

不能创建未在 schema.xml 中配置的逻辑表            

注意:如何部署

1、停机部署法:大致思路就是半夜停机升级,服务停了,跑数据迁移程序,进行数据迁移。
        写一个迁移程序,读db-old数据库,通过中间件写入新库db-new1和db-new2。
        校验迁移前后一致性,没问题就切该部分业务到新库
缺点:只能是访问量不高的,而且部署或有问题,没部署好,早上又要把数据切回去,晚上有的忙,身心累。

2、双写部署1
        上线一段代码:和表有关业务之前加一段代码,同时往老新库写。
                历史数据:在该次部署前,数据库表test_tb的有关数据,我们称之为历史数据。
                增量数据:在该次部署后,数据库表test_tb的新产生的数据,我们称之为增量数据。
        多加一条往消息队列中发消息的代码,只是写的sql。
        等到db-old中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。将迁移程序下线,写一段订阅程序订阅消息队列中的数据,订阅程序将订阅到到数据,通过中间件写入新库,新老库一致性验证,去除代码中的双写代码,将涉及到test_tb表的读写操作,指向新库。
3、双写部署2
        上面方式造成了严重的代码入侵。将非业务代码嵌入业务代码。
        替代办法是,记录日志。往消息队列里发的消息,都是写操作的消息。而binlog日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。
        打开binlog日志,迁移好历史数据后,写一个订阅程序,订阅binlog(mysql中有canal)。然后将订阅到的数据通过中间件,写入新库。

MyCat配置文件介绍

Mycat的配置文件都在conf目录里面,这里介绍几个常用的文件:

server.xml        Mycat的配置文件,设置账号、参数等
schema.xml        Mycat对应的物理数据库和数据库表的配置
rule.xml        Mycat分片(分库分表)规则

server.xml

配置 Mycat 服务信息,如: Mycat中的用户,用户可以访问的逻辑库/表,服务的端口号等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<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">2</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">1</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="root" defaultAccount="true"><!--user-->
<property name="password">123456</property>
<property name="schemas">TESTDB</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">TESTDB</property>
<property name="readOnly">true</property>
</user>
</mycat:server>

rule.xml

用于定义分片规则的配置文件,主要是查看,很少修改。里边定义了很多分配规则比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<tableRule name="auto-sharding-long">
<rule>
<columns>id</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>
........
<tableRule name="crc32slot">
<rule>
<columns>id</columns>
<algorithm>crc32slot</algorithm>
</rule>
</tableRule>

auto-sharding-long:mycat 默认的分片规则:以 500 万为单位,实现分片规则。逻辑库 A 对应 dataNode - db1 和 db2. 1-500 万保存在 db1 中, 500 万零 1 到 1000 万保存在 db2 中,1000 万零 1 到 1500 万保存在 db1 中,依次类推。
crc32slot:在 CRUD 操作时,根据具体数据的 crc32 算法计算,数据应该保存在哪一个dataNode 中. 算法类似模运算。

server.xml

用于定义逻辑库和逻辑表的配置文件.在配置文件中可以定义读写分离,逻辑库,逻辑表,dataHost,dataNode 等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100">
<table name="travelrecord" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
<table name="hotnews" primaryKey="ID" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="mod-long" />
<table name="dual" primaryKey="ID" dataNode="dnx,dnoracle2" type="global" needAddLimit="false"/>
<table name="customer" primaryKey="ID" dataNode="dn1,dn2" rule="sharding-by-intfile">
<childTable name="orders" primaryKey="ID" joinKey="customer_id" parentKey="id">
<childTable name="order_items" joinKey="order_id" parentKey="id" />
</childTable>
<childTable name="customer_addr" primaryKey="ID" joinKey="customer_id" parentKey="id" />
</table>
</schema>
<dataNode name="dn1$0-743" dataHost="localhost1" database="db$0-743"/>
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="localhost:3306" user="root" password="123456">
<readHost host="hostS2" url="192.168.1.200:3306" user="root" password="xxx" />
</writeHost>
<writeHost host="hostS1" url="localhost:3316" user="root" password="123456" />
</dataHost>
</mycat:schema>

配置解释

schema:数据库设置,此数据库为逻辑数据库
name        逻辑数据库名,与server.xml中的schema对应
checkSQLschema        数据库前缀相关设置,是否检测SQL语法的schema信息,比如逻辑库名称是D,SQL:select * from D.table 当为true时:mycat发送到数据库的sql是select * from table;false时:发送的是select * from A.table

sqlMaxLimit select 时默认的limit,Mycat 在执行 SQL 的时候,如果 SQL 语法中没有 limit 子句.自动增加 limit 子句.避免查询全表

table标签:定义逻辑表,要求逻辑表和物理表的名字一致。
name        表名,物理数据库中表名
dataNode        表存储到哪些节点,多个节点用逗号分隔。节点为下文dataNode设置的name,多个也就分库了
primaryKey        主键字段名,自动生成主键时需要设置
autoIncrement          是否自增
rule        使用分片规则名,规则都在rule.xml。SQL 语句发送到 Mycat 中后,Mycat 如何计算,应该将当期的 SQL 发送到哪一个物理数
              据库管理系统或物理 database 中
ruleRequired    表是否绑定分片规则,如果配置为 true,但没有配置具体 rule 的话 ,程序会报错。
type   逻辑表的类型,目前逻辑表只有"全局表"和"普通表"两种类型。全局表:global。
       childTable 标签
       name 定义子表的表名。
       joinKey 插入子表的时候会使用这个列的值查找父表存储的数据节点。
parentKey 属性指定的值一般为与父表建立关联关系的列名。程序首先获取 joinkey 的值,再通过 parentKey 属性指定的列名产生查询语句,通过执行该语句得到父表存储在哪个分片上。从而确定子表存储的位置。

dataNode   分片信息,也就是分库相关配置
name        节点名,定义的逻辑名称,对应具体的物理数据库 database
datahost        物理数据库名,代表使用的物理数据库所在位置和配置信息,与datahost中name对应
database        物理数据库中数据库名

dataHost   物理数据库,真正存储数据的数据库
name        物理数据库名,与dataNode中dataHost对应
balance        均衡负载的方式
       balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的 writeHost 上。
       balance="1",全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡,简单的说,当双   主双从模式(M1->S1,M2->S2,并且 M1 与 M2 互为主备),正常情况下,M2,S1,S2 都参与 select 语句的负载均衡。
       balance="2",所有读操作都随机的在 writeHost、readhost 上分发。
       balance="3",所有读请求随机的分发到 wiriterHost 对应的 readhost 执行,writerHost 不负担读压力,注意 balance=3 只在 1.4 及其以后版本有,1.3 没有。

maxCon/minCon  最大最小连接数
writeType        负载均衡类型,目前的取值有 3 种:

1. writeType="0", 所有写操作发送到配置的第一个 writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后已切换后的为准,切换记录在配置文件中:dnindex.properties .
2. writeType="1",所有写操作都随机的发送到配置的 writeHost,1.5 以后废弃不推荐。

dbType        数据库类型
dbDriver     数据库驱动类型,,native,使用 mycat 提供的本地驱动
switchType         
       -1 表示不自动切换
       1 默认值,自动切换 可能有IO延迟问题
       2 基于 MySQL 主从同步的状态决定是否切换   心跳语句为 show slave status
       3 基于 MySQL galary cluster 的切换机制(适合集群) 心跳语句为 show status like 'wsrep%'
heartbeat        心跳检测语句,注意语句结尾的分号要加。
       子标签 writeHost:写数据的数据库定义标签. 实现读写分离操作,下面的readHost表示只读
host 数据库命名
url  数据库访问路径
user  数据库访问用户名
password  密码