利用Mycat分库分表操作

为什么分库分表

1 什么是分库分表?

其实就是字面意思,很好理解:

  • 分库:从单个数据库拆分成多个数据库的过程,将数据散落在多个数据库中。
  • 分表:从单张表拆分成多张表的过程,将数据散落在多张表内。

2 为什么要分库分表?

关键字:提升性能、增加可用性。

1) 从性能上看

随着单库中的数据量越来越大、数据库的查询QPS越来越高,相应的,对数据库的读写所需要的时间也越来越多。数据库的读写性能可能会成为业务发展的瓶颈。对应的,就需要做数据库性能方面的优化。本文中我们只讨论数据库层面的优化,不讨论缓存等应用层优化的手段。

如果数据库的查询QPS过高,就需要考虑拆库,通过分库来分担单个数据库的连接压力。比如,如果查询QPS为3500,假设单库可以支撑1000个连接数的话,那么就可以考虑拆分成多个个库,来分散查询连接压力。

如果单表数据量过大,当数据量超过一定量级后,无论是对于数据查询还是数据更新,在经过索引优化等纯数据库层面的传统优化手段之后,还是可能存在性能问题。这是量变产生了质变,这时候就需要去换个思路来解决问题,比如:从数据生产源头、数据处理源头来解决问题,既然数据量很大,那我们就来个分而治之,化整为零。这就产生了分表,把数据按照一定的规则拆分成多张表,来解决单表环境下无法解决的存取性能问题。

2) 从可用性上看

单个数据库如果发生意外,很可能会丢失所有数据。尤其是云时代,很多数据库都跑在虚拟机上,如果虚拟机/宿主机发生意外,则可能造成无法挽回的损失。因此,除了传统的 Master-Slave、Master-Master 等部署层面解决可靠性问题外,我们也可以考虑从数据拆分层面解决此问题。

此处我们以数据库宕机为例:

  • 单库部署情况下,如果数据库宕机,那么故障影响就是100%,而且恢复可能耗时很长。
  • 如果我们拆分成2个库,分别部署在不同的机器上,此时其中1个库宕机,那么故障影响就是50%,还有50%的数据可以继续服务。
  • 如果我们拆分成4个库,分别部署在不同的机器上,此时其中1个库宕机,那么故障影响就是25%,还有75%的数据可以继续服务,恢复耗时也会很短。

当然,我们也不能无限制的拆库,这也是牺牲存储资源来提升性能、可用性的方式,毕竟资源总是有限的。

3. 如何分库分表

1) 分库?分表?还是既分库又分表?

从第一部分了解到的信息来看,分库分表方案可以分为下面3种:

切分方案解决的问题
只分库部分表数据库读/写QPS过高,数据库连接不足
只分表部分库单表数据量过大,存储性能遇到瓶颈
即分库又分表连接数不足+数据量过大引起的存储性能瓶颈

2) 如何选择我们自己的切分方案?

如果需要分表,那么分多少张表合适?

由于所有的技术都是为业务服务的,那么,我们就先从数据方面回顾下业务背景。

比如,我们这个业务系统是为了解决会员的咨询诉求,通过我们的XSpace客服平台系统来服务会员,目前主要以同步的离线工单数据作为我们的数据源来构建自己的数据。

假设,每一笔离线工单都会产生对应一笔会员的咨询问题(我们简称:问题单),如果:

  • 在线渠道:每天产生 3w 笔聊天会话,假设,其中50%的会话会生成一笔离线工单,那么每天可生成 3w * 50% = 1.5w 笔工单;
  • 热线渠道:每天产生 2.5w 通电话,假设,其中80%的电话都会产生一笔工单,那么每天可生成 2.5w * 80% = 2w 笔/天;
  • 离线渠道:假设离线渠道每天直接生成 3w 笔;

合计共 1.5w + 2w + 3w = 6.5w 笔/天

考虑到以后可能要继续覆盖的新的业务场景,需要提前预留部分扩展空间,这里我们假设为每天产生 8w 笔问题单。

除问题单外,还有另外2张常用的业务表:用户操作日志表、用户提交的表单数据表。

其中,每笔问题单都会产生多条用户操作日志,根据历史统计数据来可以看到,平均每个问题单大约会产生8条操作日志,我们预留一部分空间,假设每个问题单平均产生约10条用户操作日志。

如果系统设计使用年限5年,那么问题单数据量大约 = 5年 365天/年 8w/天 = 1.46亿,那么估算出的表数量如下:

  • 问题单需要:1.46亿/500w = 29.2 张表,我们就按 32 张表来切分;
  • 操作日志需要 :32 10 = 320 张表,我们就按 32 16 = 512 张表来切分。

3)如果需要分库,那么分多少库合适?

分库的时候除了要考虑平时的业务峰值读写QPS外,还要考虑到诸如双11大促期间可能达到的峰值,需要提前做好预估。

根据我们的实际业务场景,问题单的数据查询来源主要来自于阿里客服小蜜首页。因此,可以根据历史QPS、RT等数据评估,假设我们只需要3500数据库连接数,如果单库可以承担最高1000个数据库连接,那么我们就可以拆分成4个库。

4) 如何对数据进行切分?

根据行业惯例,通常按照 水平切分、垂直切分 两种方式进行切分,当然,有些复杂业务场景也可能选择两者结合的方式。

(1)水平切分

这是一种横向按业务维度切分的方式,比如常见的按会员维度切分,根据一定的规则把不同的会员相关的数据散落在不同的库表中。由于我们的业务场景决定都是从会员视角进行数据读写,所以,我们就选择按照水平方式进行数据库切分。

(2)垂直切分

垂直切分可以简单理解为,把一张表的不同字段拆分到不同的表中。

比如:假设有个小型电商业务,把一个订单相关的商品信息、买卖家信息、支付信息都放在一张大表里。可以考虑通过垂直切分的方式,把商品信息、买家信息、卖家信息、支付信息都单独拆分成独立的表,并通过订单号跟订单基本信息关联起来。

也有一种情况,如果一张表有10个字段,其中只有3个字段需要频繁修改,那么就可以考虑把这3个字段拆分到子表。避免在修改这3个数据时,影响到其余7个字段的查询行锁定。

分库

数据库准备

此处模拟东软云医院管理系统当数据库连接压力过大时进行数据库的拆分,计划拆分成两个数据库。

数据库设计关系如下:

根据上述业务表关联关系科室、用户表、挂号信息表三个表之间是有关联关系的所以应该放到一个数据节点上,另外两张表为了测试放到另外一个数据节点上,表示成如下形式:

1 停止之前的数据库

分库一定要在新的数据库上准备,此处抛弃之前数据库(之前如果你有别的请先停止掉)重新创建干净的数据库

 

[root@mycat ~]# docker stop m1 && docker stop m2 && docker stop s1 && docker stop s2
m1
m2
s1
s2

2 安装两个数据库服务(容器)

创建两个数据库,此处暂时不配置主从数据库复制(Master-Slave模式)所以不需要在docker宿主机上单独映射配置文件,执行如下命令直接创建2个MySQL数据库容器

docker run --name dn1 -p 3316:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7 &&\

docker run --name dn2 -p 3326:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

具体执行过程如下:

账户是root 密码是root

[root@mycat ~]# docker run --name dn1 -p 3316:3306   -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7 &&\
> docker run --name dn2 -p 3326:3306   -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
b9b3297b30ae82cd25889414e76e16c0fa20c186535a1c6b0d2d15469163b40e
e61487a76ef284927f031897ac38d7c6d00c88c191265f946fcf6bb237f04f54
[root@mycat ~]#

查看两个数据库机器(容器)的ip

[root@mycat ~]# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
fd2bea0b1f01   mysql:5.7   "docker-entrypoint.s…"   5 seconds ago   Up 3 seconds   33060/tcp, 0.0.0.0:3326->3306/tcp, :::3326->3306/tcp   dn2
6ae712c5f59c   mysql:5.7   "docker-entrypoint.s…"   6 seconds ago   Up 5 seconds   33060/tcp, 0.0.0.0:3316->3306/tcp, :::3316->3306/tcp   dn1


[root@mycat ~]# docker inspect --format '{{ .NetworkSettings.IPAddress }}'  dn1  &&\
> docker inspect --format '{{ .NetworkSettings.IPAddress }}'  dn2
172.17.0.2
172.17.0.3
[root@mycat ~]# 

ip地址分别是:

容器IP
dn1172.17.0.2
dn2172.17.0.3

3.创建数据库

在两个空白数据库机器(容器)上创建数据库语句如下:

CREATE DATABASE his_mycat DEFAULT CHARACTER SET utf8mb4;

[root@mycat ~]# mysql -u root -proot -h 172.17.0.3 -P3306
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.34 MySQL Community Server (GPL)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> CREATE DATABASE his_mycat DEFAULT CHARACTER SET utf8mb4;
Query OK, 1 row affected (0.01 sec)

MySQL [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| his_mycat          |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

MySQL [(none)]> exit
Bye
[root@mycat ~]# 

 

配置mycat

分库规则:

  • dn1:department部门表、user用户表、register患者挂号表
  • dn2:drugs 药品表、disease 疾病表

修改mycat的schema.xml重新配置分库规则

[root@localhost ~]# 
[root@localhost ~]# cd /opt
[root@localhost opt]# cd /usr/local/mycat/
您在 /var/spool/mail/root 中有新邮件
[root@localhost mycat]# cd conf
[root@localhost conf]# ls
autopartition-long.txt       rule.xml
auto-sharding-long.txt       schema.xml
auto-sharding-rang-mod.txt   sequence_conf.properties
cacheservice.properties      sequence_db_conf.properties
dnindex.properties           sequence_distributed_conf.properties
ehcache.xml                  sequence_time_conf.properties
index_to_charset.properties  server.xml
log4j2.xml                   sharding-by-enum.txt
migrateTables.properties     wrapper.conf
myid.properties              zkconf
partition-hash-int.txt       zkdownload
partition-range-mod.txt
[root@localhost conf]# vi schema.xml 

完整的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="drugs" dataNode="dn2"></table>
        <table name="disease" dataNode="dn2"></table>
    </schema>

    <dataNode name="dn1" dataHost="host1" database="his_mycat"/>
    <dataNode name="dn2" dataHost="host2" database="his_mycat"/>

    <dataHost name="host1" maxCon="1000" minCon="10" balance="0"
              writeType="0" dbType="mysql" dbDriver="native" switchType="1"
              slaveThreshold="100">
        <heartbeat>select user()</heartbeat>
        <!-- can have multi write hosts -->
        <writeHost host="hostM1" url="172.17.0.2:3306" user="root" password="root">
        </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>
        <!-- can have multi write hosts -->
        <writeHost host="hostM2" url="172.17.0.3:3306" user="root"  password="root">
        </writeHost>
    </dataHost>

</mycat:schema>

 启动mycat

在mycat/bin目录中执行

[root@localhost opt]# cd /usr/local/mycat/
[root@localhost mycat]# cd bin
[root@localhost bin]# ./mycat console
Running Mycat-server...
Removed stale pid file: /usr/local/mycat/logs/mycat.pid
wrapper  | --> Wrapper Started as Console
wrapper  | Launching a JVM...
jvm 1    | Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=64M; support was removed in 8.0
jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
jvm 1    | 
jvm 1    | 
jvm 1    | WrapperSimpleApp: Encountered an error running main: java.lang.ExceptionInInitializerError
jvm 1    | java.lang.ExceptionInInitializerError
jvm 1    | 	at io.mycat.MycatStartup.main(MycatStartup.java:53)
jvm 1    | 	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    | 	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    | 	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    | 	at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    | 	at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1    | 	at java.lang.Thread.run(Thread.java:748)
jvm 1    | Caused by: io.mycat.config.util.ConfigException: org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 1; 前言中不允许有内容。
jvm 1    | 	at io.mycat.config.loader.xml.XMLSchemaLoader.load(XMLSchemaLoader.java:126)
jvm 1    | 	at io.mycat.config.loader.xml.XMLSchemaLoader.<init>(XMLSchemaLoader.java:83)
jvm 1    | 	at io.mycat.config.loader.xml.XMLSchemaLoader.<init>(XMLSchemaLoader.java:87)
jvm 1    | 	at io.mycat.config.ConfigInitializer.<init>(ConfigInitializer.java:74)
jvm 1    | 	at io.mycat.config.MycatConfig.<init>(MycatConfig.java:72)
jvm 1    | 	at io.mycat.MycatServer.<init>(MycatServer.java:144)
jvm 1    | 	at io.mycat.MycatServer.<clinit>(MycatServer.java:96)
jvm 1    | 	... 7 more
jvm 1    | Caused by: org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 1; 前言中不允许有内容。
jvm 1    | 	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:204)
jvm 1    | 	at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.fatalError(ErrorHandlerWrapper.java:178)

 登录mycat创建表结构

[root@localhost ~]# mysql -umycat -p123456 -h127.0.0.1 -P8066
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.29-mycat-1.6-RELEASE-20161028204710 MyCat Server (OpenCloundDB)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show databases;
+----------+
| DATABASE |
+----------+
| TESTDB   |
+----------+
1 row in set (0.01 sec)

MySQL [(none)]> use TESTDB;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [TESTDB]> show tables;
+---------------------+
| Tables_in_his_mycat |
+---------------------+
| disease             |
| drugs               |
+---------------------+
2 rows in set (0.00 sec)

MySQL [TESTDB]> CREATE TABLE `department` (
    ->   `id` int(9) NOT NULL AUTO_INCREMENT COMMENT 'id',
    ->   `DeptCode` varchar(64) NOT NULL COMMENT '科室编码',
    ->   `DeptName` varchar(64) NOT NULL COMMENT '科室名称',
    ->   `DeptCategory` varchar(64) DEFAULT NULL COMMENT '科室分类',
    ->   `DeptTypeID` int(9) NOT NULL COMMENT '科室类型',
    ->   `DelMark` int(1) DEFAULT NULL COMMENT '删除标记',
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.09 sec)

MySQL [TESTDB]> CREATE TABLE `user` (
    ->   `id` int(9) NOT NULL COMMENT 'id',
    ->   `UserName` varchar(64) NOT NULL COMMENT '登录名',
    ->   `Password` varchar(64) DEFAULT NULL COMMENT '密码',
    ->   `RealName` varchar(64) NOT NULL COMMENT '真实姓名',
    ->   `UserTypeID` int(9) DEFAULT NULL COMMENT '1 - 挂号人员  2 - 门诊医生  3 - 医技医生 4 - 药房人员   5 - 财务人员  6 - 行政人员 ',
    ->   `DocTitleID` int(9) DEFAULT NULL COMMENT '医生职称',
    ->   `IsScheduling` int(9) DEFAULT NULL COMMENT '是否排班',
    ->   `DeptId` int(9) NOT NULL COMMENT '所在科室ID',
    ->   `RegistId` int(9) DEFAULT NULL COMMENT '挂号级别ID',
    ->   `DelMark` int(1) DEFAULT NULL COMMENT '删除标记',
    ->   PRIMARY KEY (`id`),
    ->   KEY `FK_科室id` (`DeptId`),
    ->   CONSTRAINT `FK_科室id` FOREIGN KEY (`DeptId`) REFERENCES `department` (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.08 sec)

MySQL [TESTDB]> CREATE TABLE `register` (
    ->   `id` int(9) NOT NULL COMMENT 'id',
    ->   `RealName` varchar(64) DEFAULT NULL COMMENT '真实姓名',
    ->   `Gender` int(9) DEFAULT NULL COMMENT '性别',
    ->   `IDnumber` varchar(18) DEFAULT NULL COMMENT '身份证号',
    ->   `BirthDate` date DEFAULT NULL COMMENT '出生日期',
    ->   `Age` int(3) DEFAULT NULL COMMENT '年龄',
    ->   `AgeType` int(9) DEFAULT NULL COMMENT '年龄类型',
    ->   `HomeAddress` varchar(64) DEFAULT NULL COMMENT '家庭住址',
    ->   `CaseNumber` varchar(64) DEFAULT NULL COMMENT '一名患者在同一医院看诊多次,根 据患者是否使用同一个病历本,确定该患者的“病历号码”是否相同。',
    ->   `VisitDate` date NOT NULL COMMENT '本次看诊日期',
    ->   `Noon` int(9) NOT NULL COMMENT '午别',
    ->   `DeptId` int(9) DEFAULT NULL COMMENT '本次挂号科室ID',
    ->   `UserId` int(9) DEFAULT NULL COMMENT '本次挂号医生id',
    ->   `IsBook` int(1) NOT NULL COMMENT '病历本要否',
    ->   `RegisterTime` datetime DEFAULT NULL COMMENT '挂号时间',
    ->   `RegisterID` int(9) NOT NULL COMMENT '挂号员ID',
    ->   `VisitState` int(9) DEFAULT NULL COMMENT '本次看诊状态',
    ->   PRIMARY KEY (`id`),
    ->   KEY `FK_医生id` (`UserId`),
    ->   CONSTRAINT `FK_医生id` FOREIGN KEY (`UserId`) REFERENCES `user` (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.03 sec)

MySQL [TESTDB]> CREATE TABLE `disease` (
    ->   `id` int(9) NOT NULL COMMENT 'id',
    ->   `DiseaseCode` varchar(64) DEFAULT NULL COMMENT '疾病助记编码',
    ->   `DiseaseName` varchar(255) DEFAULT NULL COMMENT '疾病名称',
    ->   `DiseaseICD` varchar(64) DEFAULT NULL COMMENT '国际ICD编码',
    ->   `DiseaseType` varchar(64) DEFAULT NULL COMMENT '疾病所属分类',
    ->   `DelMark` int(1) DEFAULT NULL COMMENT '删除标记',
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.03 sec)

MySQL [TESTDB]> CREATE TABLE `drugs` (
    ->   `id` int(9) NOT NULL COMMENT 'id',
    ->   `Drugs_Code` char(14) DEFAULT NULL COMMENT '药品编码',
    ->   `Drugs_Name` varchar(64) DEFAULT NULL COMMENT '药品名称',
    ->   `Drugs_Format` varchar(64) DEFAULT NULL COMMENT '药品规格',
    ->   `Drugs_Unit` varchar(64) DEFAULT NULL COMMENT '包装单位',
    ->   `Manufacturer` varchar(512) DEFAULT NULL COMMENT '生产厂家',
    ->   `Drugs_Dosage` varchar(64) DEFAULT NULL COMMENT '药品剂型',
    ->   `Drugs_Type` varchar(64) DEFAULT NULL COMMENT '药品类型',
    ->   `Drugs_Price` decimal(8,2) DEFAULT NULL COMMENT '药品单价',
    ->   `Mnemonic_Code` varchar(64) DEFAULT NULL COMMENT '拼音助记码',
    ->   `Creation_Date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    ->   `DelMark` int(1) DEFAULT NULL COMMENT '有效性标记',
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.03 sec)

MySQL [TESTDB]> show tables;
+---------------------+
| Tables_in_his_mycat |
+---------------------+
| disease             |
| drugs               |
| department          |
| register            |
| user                |
+---------------------+
5 rows in set (0.01 sec)

 

验证

创建完成后分别在mycat客户端、dn1节点、dn2节点查看表存储情况

dn1表为 department        register         user   

 dn2表为disease    drugs  

 

 

分表

 

请看下节解析!

请看下节解析!

请看下节解析!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值