Mysql数据切分
1、什么是数据切分
据Mysql官方文档的介绍,Mysql一张表的理论存储上限是(232)2条数据,但它还受myisam_data_pointer_size的值的影响,默认值为6,即是48位,所以行数是2^48-1条数据,另外还受表的存储空间大小限制(256TB)。
但在实践中,是存储不到这么多的数据的,即使存储了这么多数据,在查询的时候也会由于数据量庞大导致查询的速度缓慢。于是,数据切分在此基础上产生。
数据切分可分为垂直切分和水平切分。
1.1、垂直切分
垂直切分又分为垂直分库和垂直分表。
【垂直分库】
垂直分库就是将数据库中关联度低的表存放在其他数据库。
【垂直分表】
垂直分表就是将表中耦合度低的列拆分开,变成一个只有不常用的字段的表,只留下耦合度高的列。如学生信息表,有学号,姓名,专业,手机号,身份证号,其中可以将手机号和身份证号拆分开,留下学号、姓名和专业。
【优点】
在高并发场景下,可以缓解单机性能不足的问题,还可以提高sql语句的效率
【缺点】
分库后无法进行join操作,事务操作也过于复杂。
1.2、水平切分
垂直切分可以解决字段庞大和数据表太多导致的效率低的问题,而水平切分就是将海量的数据进行切分。
水平切分也可以分为水平分库和水平分表。
【水平分库】
水平分库就是将单个数据库拆分成多个数据库。
【水平分表】
水平分表就是将单张数据表中的数据根据某种规则拆分成多张数据表。
如学生信息表
id | name | |
---|---|---|
1 | 1 | zhangsan |
...... | ...... | ...... |
100 | 100 | lisi |
...... | ...... | ...... |
200 | 200 | wangwu |
...... | ...... | ...... |
300 | 300 | zhaoliu |
可以根据id来将它拆分成
表1
id | name | |
---|---|---|
1 | 1 | zhangsan |
...... | ...... | ...... |
表2
id | name | |
---|---|---|
101 | 101 | lisi |
...... | ...... | ...... |
表3
id | name | |
---|---|---|
201 | 201 | wangwu |
...... | ...... | ...... |
表4
id | name | |
---|---|---|
301 | 301 | zhaoliu |
...... | ...... | ...... |
【优点】
数据库的负载变小,sql语句的效率提升。
2、数据分片规则
数据分片规则大部分是应用在水平分表上的,按照某种规则将一张大型的数据表拆分成多张小表。常见的分片规则有Hash取模、数值Range和一致性Hash算法。
2.1、Hash取模
假如有一张学生信息表
id | name | |
---|---|---|
1 | 1 | zhangsan |
2 | 2 | lisi |
3 | 3 | wangwu |
4 | 4 | zhaoliu |
将它拆分成两张表,可以根据id%2来拆分:
1%2=1,于是将id为1的记录存放在表1中,2%2=0,于是将id为2的记录存放在表0中。
表0:
id | name | |
---|---|---|
2 | 2 | lisi |
4 | 4 | zhaoliu |
表1:
id | name | |
---|---|---|
1 | 1 | zhangsan |
3 | 3 | wangwu |
【Hash取模的优点】
每张数据表的数据比较均匀,难出现访问的瓶颈。
【Hash取模的缺点】
后期扩容时,数据的迁移较难。
在上面的例子中,如果查询条件中不带id,那么将会对两张表进行查询,分表反而会拖慢速度。
【hash取模代码演示】
准备工作:修改schema.xml和rule.xml的代码
在rule.xml添加,其中3表示有三台服务器
<tableRule name="hashmod">
<rule>
<columns>id</columns>
<algorithm>hash-mod</algorithm>
</rule>
</tableRule>
<function name="hash-mod" class="io.mycat.route.function.PartitionByHashMod">
<property name="count">3</property>
</function>
在schema.xml中配置
<table name="test_table" dataNode="dn1,dn2,dn3" rule="hashmod" />
在IDEA中编辑代码
@Test
public void hashmod() throws SQLException, ClassNotFoundException {
//插入一个随机id
Long num = Math.round(Math.random()*10000+1);
//数据库配置
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, username, password);
//使用explain语句查询该条插入语句会进入哪个数据库
PreparedStatement show_tables = conn.prepareStatement("explain insert into TEST_TABLE (id,name) values("+ num +",'lxy')");
ResultSet resultSet = show_tables.executeQuery();
List list = new ArrayList<>();
//遍历返回值
while (resultSet.next()) {
ResultSetMetaData rsMeta = resultSet.getMetaData();
int columnCount = rsMeta.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
list.add(rsMeta.getColumnLabel(i) + resultSet.getObject(i));
//判断resultSet.getObject(i)的值,只有dn1,dn2,dn3三种,代表三个数据库节点
switch (resultSet.getObject(i).toString()) {
case "dn1":
System.out.println("插入的是dn1数据库!");
break;
case "dn2":
System.out.println("插入的是dn2数据库!");
break;
case "dn3":
System.out.println("插入的是dn3数据库!");
break;
}
}
}
PreparedStatement show_tables1 = conn.prepareStatement("insert into TEST_TABLE (id,name) values("+ num +",'lxy')");
show_tables1.executeUpdate();
System.out.println(list);
}
运行效果:
2.2、数值Range
假如有一张学生信息表
id | name | |
---|---|---|
1 | 1 | zhangsan |
...... | ...... | ...... |
100 | 100 | lisi |
...... | ...... | ...... |
200 | 200 | wangwu |
...... | ...... | ...... |
300 | 300 | zhaoliu |
数值Range就是将某个范围的记录分别放在表0,表1,表2等。
比如可以根据id来将它拆分成
表1
id | name | |
---|---|---|
1 | 1 | zhangsan |
...... | ...... | ...... |
表2
id | name | |
---|---|---|
101 | 101 | lisi |
...... | ...... | ...... |
表3
id | name | |
---|---|---|
201 | 201 | wangwu |
...... | ...... | ...... |
表4
id | name | |
---|---|---|
301 | 301 | zhaoliu |
...... | ...... | ...... |
这是将1-100,101-200,201-300,301-...的id拆分成4张表。
【数值Range的优点】
单张表的记录的条数是固定的。
水平扩展容易,无需对数据进行迁移。
在查找时可以根据每张表的记录条数来精准定位。
【数值Range的缺点】
热门的数据的性能会下降,如果频繁地查询1-100的记录,而其他的数据没有进行频繁地查询,会导致1-100的速度变慢。
【数值Range代码演示】
准备工作:修改schema.xml和autopartition-long.txt的代码
在autopartition-long.txt添加,其中0-10k表示0-10000的数值插入到1节点,以此类推
# range start-end ,data node index
# K=1000,M=10000.
0-10k=1
10k-20k=2
20k-1000M=3
在schema.xml中配置
<table name="test_table" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
在IDEA中编辑代码
@Test
public void range() throws SQLException, ClassNotFoundException {
//插入三个id
int num1 = 9999, num2 = 19999, num3 = 29999;
//数据库配置
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, username, password);
//使用explain语句查询该条插入语句会进入哪个数据库
PreparedStatement show_tables = conn.prepareStatement("explain insert into TEST_TABLE (id,name) values(" + num1 + ",'lxy')"
+",("+num2+",'lxy')"
+",("+num3+",'lxy')");
ResultSet resultSet = show_tables.executeQuery();
List list = new ArrayList<>();
//遍历返回值
while (resultSet.next()) {
ResultSetMetaData rsMeta = resultSet.getMetaData();
int columnCount = rsMeta.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
list.add(rsMeta.getColumnLabel(i) + resultSet.getObject(i));
//判断resultSet.getObject(i)的值,只有dn1,dn2,dn3三种,代表三个数据库节点
switch (resultSet.getObject(i).toString()) {
case "dn1":
System.out.println("插入的是dn1数据库!");
break;
case "dn2":
System.out.println("插入的是dn2数据库!");
break;
case "dn3":
System.out.println("插入的是dn3数据库!");
break;
}
}
}
PreparedStatement show_tables1 = conn.prepareStatement("insert into TEST_TABLE (id,name) values(" + num1 + ",'lxy')"
+",("+num2+",'lxy')"
+",("+num3+",'lxy')");
show_tables1.executeUpdate();
System.out.println(list);
}
由于插入了三个区间的值,所以结果应该是输出插入了三个不同的数据库。
运行结果:
2.3、一致性Hash算法
可以参考我的另一篇随笔:一致性Hash算法
【一致性hash算法代码演示】
准备工作:修改rule.xml和schema.xml中的代码
rule.xml中已经配置好了一致性hash算法的实现类,把节点修改成3即可
<tableRule name="sharding-by-murmur">
<rule>
<columns>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">3</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>
schema.xml中配置
<table name="test_table" dataNode="dn1,dn2,dn3" rule="sharding-by-murmur" />
在IDEA中编辑代码,代码和hash取模算法一样
@Test
public void murmur() throws SQLException, ClassNotFoundException {
//插入一个随机id
Long num = Math.round(Math.random() * 10000 + 1);
//数据库配置
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, username, password);
//使用explain语句查询该条插入语句会进入哪个数据库
PreparedStatement show_tables = conn.prepareStatement("explain insert into TEST_TABLE (id,name) values(" + num + ",'lxy')");
ResultSet resultSet = show_tables.executeQuery();
List list = new ArrayList<>();
//遍历返回值
while (resultSet.next()) {
ResultSetMetaData rsMeta = resultSet.getMetaData();
int columnCount = rsMeta.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
list.add(rsMeta.getColumnLabel(i) + resultSet.getObject(i));
//判断resultSet.getObject(i)的值,只有dn1,dn2,dn3三种,代表三个数据库节点
switch (resultSet.getObject(i).toString()) {
case "dn1":
System.out.println("插入的是dn1数据库!");
break;
case "dn2":
System.out.println("插入的是dn2数据库!");
break;
case "dn3":
System.out.println("插入的是dn3数据库!");
break;
}
}
}
PreparedStatement show_tables1 = conn.prepareStatement("insert into TEST_TABLE (id,name) values(" + num + ",'lxy')");
show_tables1.executeUpdate();
System.out.println(list);
}
运行结果:
问题解决
Q1:使用IDEA进行数据表的查询时,有时能够查询成功,有时总会提示mycat ERROR 1105 (HY000): Table 'springboot_mysql1.TEST_TABLE' doesn't exist。
A1:因为只给3307和3308节点创建了相对应的数据库,而3309和3310节点没有创建对应的数据库,所以在查询3309和3310节点时,会提示表不存在,而有时会在3307和3308节点查询,所以能够查询成功。解决办法:在schema.xml中关闭3309和3310的节点。