MyCat的配置与数据分片
MyCat 下载
官网:Mycat 数据库中间件
源码下载:点我直接下载
配置启动参数
首先运行一下MycatStartup
然后配置
1 | #环境变量 |
在 Mycat 有核心三个配置文件,分别为:sever.xml、schema.xml、rule.xml
- sever.xml:是 Mycat 服务器参数调整和用户授权的配置文件
- schema.xml:是逻辑库定义和表以及分片定义的配置文件
- rule.xml:是分片规则的配置文件,分片规则的具体一些参数信息单独存放为文件,也在这个目录下,配置文件修改需要重启
Mycat 核心概念
- 逻辑库:Mycat 中的虚拟数据库。对应实际数据库的概念。在没有使用 mycat 时,应用需要确定当前连接的数据库等信息,那么当使用 mycat 后,也需要先虚拟一个数据库,用于应用的连接。
- 逻辑表:mycat 中的虚拟数据表。对应时间数据库中数据表的概念。
- 非分片表:没有进行数据切分的表。
- 分片表:已经被数据拆分的表,每个分片表中都有原有数据表的一部分数据。多张分片表可以构成一个完整数据表。
- ER 表:子表的记录与所关联的父表记录存放在同一个数据分片上,即子表依赖于父表,通过表分组(Table Group)保证数据 Join 不会跨库操作。表分组(Table Group)是解决跨分片数据 join 的一种很好的思路,也是数据切分规划的重要一条规则
- 全局表:可以理解为是一张数据冗余表,如状态表,每一个数据分片节点又保存了一份状态表数据。数据冗余是解决跨分片数据 join 的一种很好的思路,也是数据切分规划的另外一条重要规则。
- 分片节点(dataNode):数据切分后,每一个数据分片表所在的数据库就是分片节点。
- 节点主机(dataHost):数据切分后,每个分片节点(dataNode)不一定都会独占一台机器,同一机器上面可以有多个分片数据库,这样一个或多个分片节点(dataNode)所在的机器就是节点主机(dataHost),为了规避单节点主机并发数限制,尽量将读写压力高的分片节点(dataNode)均衡的放在不同的节点主机(dataHost)。
- 分片规则(rule):按照某种业务规则把数据分到某个分片的规则就是分片规则。
- 全局序列号(sequence):也可以理解为分布式 id。数据切分后,原有的关系数据库中的主键约束在分布式条件下将无法使用,因此需要引入外部机制保证数据唯一性标识,这种保证全局性的数据唯一标识的机制就是全局序列号(sequence),如 UUID、雪花算法等。
环境准备
在server.xml
文件中的 system 标签下配置所有的参数,全部为环境参数,可以根据当前需要进行开启和配置
如:设置 mycat 连接端口号:默认端口:8066
1 | <property name="serverPort">8066</property> |
通过 Mycat 访问数据库
server.xml 配置
应用连接 mycat 的话,也需要设置用户名、密码、被连接数据库信息,要配置这些信息的话,可以修改 server.xml,在其内部添加内容如下:
1 | <!--配置自定义用户信息--> |
schema.xml 配置
当配置了一个虚拟数据库后,还需要修改 schema.xml,对虚拟库进行详细配置
schema.xml 中分为三部分内容:逻辑库名字对应的真实库的名字,真实库对应具体的连接地址
1 |
|
访问测试
通过 navicat 创建本地数据库连接并创建对应数据库,同时创建 mycat 连接。 在 mycat 连接中操作表,添加数据,可以发现,本地数据库中同步的也新增了对应的数据。
数据分片
1、取模分片
当一个数据表中的数据量非常大时,就需要考虑对表内数据进行分片,拆分的规则有很多种,比较简单的一种就是,通过对 id 进行取模,完成数据分片。
修改 schema.xml
table 标签新增属性:subTables
、rule
1 |
|
修改 rule.xml
在 schema.xml 中已经指定规则为 mod-long。因此需要到该文件中修改对应信息。
1 | <tableRule name="mod-long"> |
新增数据测试
向mycat
的userDB
数据库中插入 9 条数据
1 | INSERT INTO t_user(id) VALUES(1); |
问题分析
散列不均匀,出现数据倾斜(因为对 3 取模,但是数据量不一定能被 3 取模,比如 10 条数据)
- 每张表中的数据量差距较大(比如插入的 id 是随机的)
动态扩容时,存在重新计算,出现数据丢失(比如把对 3 取模改为对 4 取模)
- 动态扩容后新增表时,需要对模数修改时有可能就会造成当查询某个分片时,在该分片中找不到对应数据
2、全局 id 分片
当进行数据切分后,数据会存放在多张表中,如果仍然通过数据库自增 id 的方式,就会出现 ID 重复的问题,造成数据错乱。所以当拆分完数据后,需要让每一条数据都有自己的 ID,并且在多表中不能出现重复。比较常见的会使用雪花算法来生成分布式 id。
在 Mycat 中也提供了四种方式来进行分布式 id 生成:基于文件、基于数据库、基于时间戳和基于 ZooKeeper。
基于本地文件
优点:
- 本地加载,读取速度较快
缺点:
MyCAT 重新发布后,配置文件中的 sequence 会初始化
生成的 id 没有意义
MyCat 如果存在多个,会出现 id 重复冲突
流程:
修改sequence_conf.properties
把后面的注释删掉!!!
从 1000 开始到 200000 结束,一旦从新启动又会从 1000 开始,就会重复报错。
1 | #使用过的历史分段,可不配置 = |
修改server.xml
0 是使用文件保存文件,1 是使用数据库保存文件,2 是时间戳,3 是 zookeeper
1 | <!--设置全局序号生成方式 |
测试:
插入数据
MYCATSEQ_USER 这里的 USER 就是上面
sequence_conf.properties
里定义的USER
,需要保持一致,且大写
1 | insert into t_user(id,username) values('next value for MYCATSEQ_USER','wangwu') |
其他三种配置方式:
数据库
- 在实际的物理表中执行
dbseq - utf8mb4
中的sql
语句,执行完毕后,会创建一张表
执行完成后会创建一张表,手动添加一条字段
修改
sequence_db_conf.properties
文件T_USER
需要和上面添加的字段的表明保持一致
1 | T_USER=localdn |
- 修改 server.xml 文件,修改全局序列号生成方式为数据库方式
1 | <property name="sequenceHandlerType">1</property> |
- 修改 schema.xml, 在 table 中添加自增属性
1 | <table name="t_user" dataNode="localdn" primaryKey="id" subTables="t_user$1-3" rule="mod-long" autoIncrement="true"/> |
- 在
mycat
中的userDB
数据库中执行语句
1 | insert into t_user(id,username) values('next value for MYCATSEQ_T_USER','wangwu') |
zookeeper
- 修改server.xml,更改生成模式
1 | <property name="sequenceHandlerType">3</property> |
- 修改myid.properties,配置 zookeeper 连接信息
1 | loadZk=false |
- 修改sequence_distributed_conf.properties
1 | INSTANCEID=ZK #声明使用zk生成 |
- 新增数据
1 | #MYCATSEQ_这个后缀可以随便写即可 |
- 特点
- ID 结构:long 64 位,ID 最大可占 63 位
- 可以承受单机房单机器单线程 1000*(2^6)=640000 的并发。
- 无悲观锁,无强竞争,吞吐量更高
时间戳
修改server.xml。更改生成方式
1
<property name="sequenceHandlerType">2</property>
修改sequence_time_conf.properties
1 | #sequence depend on TIME |
新增数据
1
2#MYCATSEQ_这个后缀可以随便写即可
insert into t_user(id,username) values('next value for MYCATSEQ_TB_BBBB','atguigu')特点
- 不存在 id 重复的现象
- 数据类型太长,建议字段类型采用 bigint(最大取值 18446744073709551615)
3、枚举分片
适用于在特定业务场景下,将不同的数据存放于不同的数据库中,如按省份存放订单、按存放人员信息等。
- 修改schema.xml,修改table标签中name属性为当前操作的表名,rule属性为sharding-by-intfile
1 | <table name="t_user" dataNode="dn129,dn130" primaryKey="id" rule="sharding-by-intfile"/> |
- 修改rule.xml,配置tableRule为sharding-by-intfile中columns属性为当前指定分片字段
1 | <tableRule name="sharding-by-intfile"> |
- 修改rule.xml中hash-int
1 | <function name="hash-int" |
- 修改partition-hash-int.txt。指定分片字段不同值存在于不同的数据库节点
1 | male=0 #代表第一个datanode |
注意事项
该方案适用于特定业务场景进行数据分片,但该方式容易出现数据倾斜,如不同省份的订单量一定会不同。订单量大的省份还会进行数据分库,数据库架构就会继续发生对应改变。
4、固定 hash 分片
固定 hash 分片的工作原理类似与redis cluster
槽的概念,在固定 hash 中会有一个范围是 0-1024,内部会进行二进制运算操作,如取 id 的二进制低 10 位 与 1111111111 进行&运算。从而当出现连接数据插入,其有可能会进入到同一个分片中,减少了分布式事务操作,提升插入效率同时尽量减少了数据倾斜问题,但不能避免不出现数据倾斜。
按照上面这张图就存在两个分区,partition1 和 partition2。partition1 的范围是 0-255,partition2 的范围是 256-1024。
当向分区中存数据时,先将 id 值转换为二进制,接着将转换过来的二进制和 1111111111
进行与运算&
,再对结果值转换为十进制,从而确定当前数据应该存入哪个分区中。
1023 的二进制&1111111111 运算后为 1023,故落入第二个分区
1024 的二进制&1111111111 运算后为 0,故落入第一个分区
266 的二进制&1111111111 运算后为 266,故落入第二个分区内
- 修改schema.xml,配置自定义固定 hash 分配规则
1 | <table name="t_user" dataNode="dn129,dn130" primaryKey="id" rule="partition-by-fixed-hash"/> |
- 修改rule.xml,配置自定义固定 hash 分片规则
1 | <tableRule name="partition-by-fixed-hash"> |
- 测试,添加数据,可以发现数据会根据计算,落入相应的数据库节点。
5、固定范围分片
该规则有点像枚举与固定 hash 的综合体,设置某一个字段,然后规定该字段值的不同范围值会进入到哪一个 dataNode。适用于明确知道分片字段的某个范围属于某个分片
优点:适用于想明确知道某个分片字段的某个范围具体在哪一个节点
缺点:如果短时间内有大量的批量插入操作,那么某个分片节点可能一下子会承受比较大的数据库压力,而别的分片节点此时可能处于闲置状态,无法利用其它节点进行分担压力(热点数据问题)
- 修改schema.xml。
1 | <table name="tb_user_range" dataNode="dn129,dn130" primaryKey="user_id" rule="auto-sharding-long"/> |
- 修改rule.xml
1 | <tableRule name="auto-sharding-long"> |
修改autopartition-long.txt,定义自定义范围
1 | #用于定义dataNode对应的数据范围,如果配置多了会报错。 |
- 测试,添加用户信息,年龄分别为 9 和 33
6、取模范围分片
这种方式结合了范围分片和取模分片,主要是为后续的数据迁移做准备。
优点:可以自主决定取模后数据的节点分布
缺点:dataNode 划分节点是事先建好的,需要扩展时比较麻烦
- 修改 schema.xml,配置分片规则
1 | <table name="t_user" dataNode="dn129,dn130" primaryKey="id" rule="sharding-by-partition"/> |
- 修改 rule.xml,添加分片规则
1 | <tableRule name="sharding-by-partition"> |
- 添加 partition-pattern.txt,(自己新建这个文件)文件内部配置节点中数据范围
1 | #0-128表示id%256后的数据范围。 |
- 测试
7、字符串 hash 分片
在业务场景下,有时可能会根据某个分片字段的前几个值来进行取模。如地址信息只取省份、姓名只取前一个字的姓等。此时则可以使用该种方式。
其工作方式与取模范围分片类型,该分片方式支持数值、符号、字母取模。
- 修改 schema.xml。
1 | <table name="tb_user_string_hash" dataNode="dn129,dn130" primaryKey="user_id" rule="sharding-by-string-hash"/> |
- 修改 rule.xml,定义拆分规则
1 | <tableRule name="sharding-by-string-hash"> |
- 新建 partition-pattern-string-hash.txt。指定数据分片节点
1 | 0-128=0 |
4)运行后可以发现 ,不同的姓名取模后,会进入不同的分片节点。
8、一致性 hash
通过一致性 hash 分片可以最大限度的让数据均匀分布,但是均匀分布也会带来问题,就是分布式事务。
原理(重点)
一致性 hash 算法引入了 hash 环的概念。环的大小是 0~2^32-1。首先通过 crc16 算法计算出数据节点在 hash 环中的位置。
当存储数据时,也会采用同样的算法,计算出数据 key 的 hash 值,映射到 hash 环上。
然后从数据映射的位置开始,以顺时针的方式找出距离最近的数据节点,接着将数据存入到该节点中。
此时可以发现,数据并没有达到预期的数据均匀,可以发现如果两个数据节点在环上的距离,决定有大量数据存入了 dataNode2,而仅有少量数据存入 dataNode1。
为了解决数据不均匀的问题,在 mycat 中可以设置虚拟数据映射节点。同时这些虚拟节点会映射到实际数据节点。
数据仍然以顺时针方式寻找数据节点,当找到最近的数据节点无论是实际还是虚拟,都会进行存储,如果是虚拟数据节点的话,最终会将数据保存到实际数据节点中。 从而尽量的使数据均匀分布。
实现流程如下:
修改 schema.xml
1
<table name="tb_user_murmur" dataNode="dn129,dn130" primaryKey="user_id" rule="sharding-by-murmur"/>
修改 rule.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<tableRule name="sharding-by-murmur">
<rule>
<columns>user_id</columns>
<algorithm>murmur</algorithm>
</rule>
</tableRule>
<function name="murmur"
class="io.mycat.route.function.PartitionByMurmurHash">
<property name="seed">0</property><!-- 默认是0即可 -->
<property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片 -->
<property name="virtualBucketTimes">160</property><!-- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍 -->
<!-- <property name="weightMapFile">weightMapFile</property> 节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,否则以1代替 -->
<!-- <property name="bucketMapPath">/etc/mycat/bucketMapPath</property>
用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,就不会输出任何东西 -->
</function>测试: 循环插入一千条数据,数据会尽量均匀的分布在两个节点中。
9、时间分片
当数据量非常大时,有时会考虑,按天去分库分表。这种场景是非常常见的。同时也有利于后期的数据查询。
修改 schema.xml
1
<table name="t_user_day" dataNode="dn129,dn130" primaryKey="id" rule="sharding-by-date"/>
修改 rule.xml,每十天一个分片,从起始时间开始计算,分片不够,则报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<tableRule name="sharding-by-date">
<rule>
<columns>create_time</columns>
<algorithm>partbyday</algorithm>
</rule>
</tableRule>
<function name="partbyday"
class="io.mycat.route.function.PartitionByDate">
<!--日期格式-->
<property name="dateFormat">yyyy-MM-dd hh:mm:ss</property>
<!-- 官方要求的默认值 -->
<property name="sNaturalDay">0</property>
<!--从哪天开始,并且只能插入2020年的数据,2021的无法插入-->
<property name="sBeginDate">2021-01-01</property>
<!--每隔几天一个分片-->
<property name="sPartionDay">10</property>
</function>测试: 当时间为 1 月 1-10 号之间,会进入 129 节点。当时间为 11-20 号之间,会进入 130 节点,当超出则报错。