Skip to content

Latest commit

 

History

History
583 lines (359 loc) · 22.4 KB

File metadata and controls

583 lines (359 loc) · 22.4 KB

Mycat应用

1.为什么要分库分表

1.1 数据库性能瓶颈的出现

1.2 数据库优化方案对比

1.2.1 SQL 与索引

因为 SQL 语句是在我们的应用端编写的,所以第一步,我们可以在程序中对 SQL 语 句进行优化,最终的目标是用到索引。这个是容易的也是最常用的优化手段。

1.2.2 表与存储引擎

第二步,数据是存放在表里面的,表又是以不同的格式存放在存储引擎中的,所以 我们可以选用特定的存储引擎,或者对表进行分区,对表结构进行拆分或者冗余处理, 或者对表结构比如字段的定义进行优化。

1.2.3 架构

第三步,对于数据库的服务,我们可以对它的架构进行优化。

如果只有一台数据库的服务器,我们可以运行多个实例,做集群的方案,做负载均衡。

或者基于主从复制实现读写分离,让写的服务都访问 master 服务器,读的请求都访 问从服务器,slave 服务器自动 master 主服务器同步数据。

或者在数据库前面加一层缓存,达到减少数据库的压力,提升访问速度的目的。

为了分散数据库服务的存储压力和访问压力,我们也可以把不同的数据分布到不同 的服务节点,这个就是分库分表(scale out)。

注意主从(replicate)和分片(shard)的区别: 主从通过数据冗余实现高可用,和实现读写分离。

分片通过拆分数据分散存储和访问压力。

1.2.4 配置

第四步,是数据库配置的优化,比如连接数,缓冲区大小等等,优化配置的目的都 是为了更高效地利用硬件。

1.2.5 操作系统与硬件

1.3 架构演进与分库分表

1.3.1 单应用单数据库

image-20220307001411426

1.3.2 多应用单数据库

image-20220307001424225

1.3.3 多应用独立数据库

image-20220307001439129

1.3.4 什么时候分表?

在分库之后,还需要进一步进行分表。当然,我们最开始想到的可能是在一 个数据库里面拆分数据,分区或者分表,到后面才是切分到多个数据库中。

分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。

image-20220307001534897

2.分库分表的类型和特点

垂直切分:基于表或字段划分,表结构不同。我们有单库的分表,也有多库的分库。

水平切分:基于数据划分,表结构相同,数据不同,也有同库的水平切分和多库的 切分。

2.1 垂直切分

垂直分表有两种,一种是单库的,一种是多库的。

2.1.1 单库垂直分表

单库分表,比如:商户信息表,拆分成基本信息表,联系方式表,结算信息表,附 件表等等。

2.1.2 多库垂直分表

多库垂直分表就是把原来存储在一个库的不同的表,拆分到不同的数据库。

image-20220307204957851

image-20220307205020795

2.2 水平切分

水平切分也可以分成两种,一种是单库的,一种是多库的。

2.2.1 单库水平分表

2.2.2 多库水平分表

另一种是多库的水平分表。比如客户表,我们拆分到多个库存储,表结构是完全一 样的。

image-20220307205210752

2.3 多案分库分表带来的问题

2.3.1 跨库关联查询

1、字段冗余

比如我们查询合同库的合同表的时候需要关联客户库的客户表,我们可以直接把一 些经常关联查询的客户字段放到合同表,通过这种方式避免跨库关联查询的问题。

2、数据同步:比如商户系统要查询产品系统的产品表,我们干脆在商户系统创建一 张产品表,通过 ETL 或者其他方式定时同步产品数据。

3、全局表(广播表) 比如行名行号信息被很多业务系统用到,如果我们放在核心 系统,每个系统都要去关联查询,这个时候我们可以在所有的数据库都存储相同的基础 数据。

4、ER 表(绑定表)

我们有些表的数据是存在逻辑的主外键关系的,比如订单表 order_info,存的是汇 总的商品数,商品金额;订单明细表 order_detail,是每个商品的价格,个数等等。或者 叫做从属关系,父表和子表的关系。他们之间会经常有关联查询的操作,如果父表的数 据和子表的数据分别存储在不同的数据库,跨库关联查询也比较麻烦。

5、系统层组装

在不同的数据库节点把符合条件数据的数据查询出来,然后重新组装,返回给客户 端。

2.3.2 分布式事务

CAP 三者是不能共有的,只能同时满足其中两点。基于 AP,我们又有了BASE理论。

基本可用(Basically Available):分布式系统在出现故障时,允许损失部分可用 功能,保证核心功能可用。

软状态(Soft state):允许系统中存在中间状态,这个状态不影响系统可用性, 这里指的是 CAP 中的不一致。

最终一致(Eventually consistent):最终一致是指经过一段时间后,所有节点数 据都将会达到一致。

分布式事务有几种常见的解决方案:

1、全局事务(比如 XA 两阶段提交;应用、事务管理器(TM)、资源管理器(DB)), 例如 Atomikos

2、基于可靠消息服务的分布式事务

image-20220307210432323

3、柔性事务 TCC(Try-Confirm-Cancel)tcc-transaction

image-20220307210548785

4、最大努力通知,通过消息中间件向其他系统发送消息(重复投递+定期校对)

2.3.3 排序、翻页、函数计算问题

2.3.4 全局主键避重问题

我们有几种常见的解决方案:

1)UUID(Universally Unique Identifier 通用唯一识别码) UUID 标准形式包含 32 个 16 进制数字,分为 5 段,形式为 8-4-4-4-12 的 36 个字 符,例如:c4e7956c-03e7-472c-8909-d733803e79a9。

image-20220307210804823

image-20220307210835883

2)数据库

把序号维护在数据库的一张表中。这张表记录了全局主键的类型、位数、起始值,当前值。当其他应用需要获得全局 ID 时,先 for update 锁行,取到值+1 后并且更新后 返回。并发性比较差。

3)Redis 基于 Redis 的 INT 自增的特性,使用批量的方式降低数据库的写压力,每次获取一 段区间的 ID 号段,用完之后再去数据库获取,可以大大减轻数据库的压力。

4)雪花算法 Snowflake(64bit)

image-20220307210958586

核心思想:

a)使用 41bit 作为毫秒数,可以使用 69 年

b)10bit 作为机器的 ID(5bit 是数据中心,5bit 的机器 ID),支持 1024 个 节点

c)12bit 作为毫秒内的流水号(每个节点在每毫秒可以产生 4096 个 ID)

d)最后还有一个符号位,永远是 0

优点:毫秒数在高位,生成的 ID 整体上按时间趋势递增;不依赖第三方系统,稳定 性和效率较高,理论上 QPS 约为 409.6w/s(1000*2^12),并且整个分布式系统内不会 产生 ID 碰撞;可根据自身业务灵活分配 bit 位

不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成 ID 重复

2.4 多数据源/读写数据源的解决方案

我们先要分析一下 SQL 执行经过的流程。

DAO——Mapper(ORM)——JDBC——代理——数据库服务

2.4.1 客户端 DAO 层

Spring 中提供了一个抽象类 AbstractRoutingDataSource,可以实现数据源的动态 切换。

1)aplication.properties 定义多个数据源

2)创建@TargetDataSource 注解

3)创建 DynamicDataSource 继承 AbstractRoutingData

4)多数据源配置类 DynamicDataSourceConfig

5)创建切面类 DataSourceAspect,对添加了@TargetDataSource 注解的 类进行拦截设置数据源。

6)在 启 动 类 上 自 动 装 配 数 据 源 配 置 @Import({DynamicDataSourceConfig.class}) 7)在 实 现 类 上 加 上 注 解 , 如 @TargetDataSource(name = DataSourceNames.SECOND),调用。

在 DAO 层实现的优势:不需要依赖 ORM 框架,即使替换了 ORM 框架也不 受影响。实现简单(不需要解析 SQL 和路由规则),可以灵活地定制。

缺点:不能复用,不能跨语言

2.4.2 ORM 框架层

第二个是在框架层,比如我们用 MyBatis 连接数据库,也可以指定数据源。我们可 以基于 MyBatis 插件的拦截机制(拦截 query 和 update 方法),实现数据源的选择。

2.4.3 驱动层

不管是MyBatis还是Hibernate,还是Spring的JdbcTemplate,本质上都是对JDBC 的封装,所以第三层就是驱动层。比如 Sharding-JDBC,就是对 JDBC 的对象进行了封 装。JDBC 的核心对象:

DataSource:数据源

Connection:数据库连接

Statement:语句对象

ResultSet:结果集

2.4.4 代理层

前面三种都是在客户端实现的,也就是说不同的项目都要做同样的改动,不同的编 程语言也有不同的实现,所以我们能不能把这种选择数据源和实现路由的逻辑提取出来, 做成一个公共的服务给所有的客户端使用呢? 这个就是第四层,代理层。比如 Mycat 和 Sharding-Proxy,都是属于这一层

2.4.5 数据库服务

3.Mycat 概念与配置

Mycat-doc/入门指南 at master · MyCATApache/Mycat-doc · GitHub

www.mycat.io

GitHub - MyCATApache/Mycat-Server

3.1 Mycat 介绍与核心

3.1.1 基本介绍

历史:从阿里 cobar 升级而来,由开源组织维护,2.0 正在开发中。

定位:运行在应用和数据库之间,可以当做一个 MySQL 服务器使用,实现对 MySQL 数据库的分库分表,也可以通过 JDBC 支持其他的数据

image-20220307212429497

Mycat 的关键特性(官网首页)

image-20220307213535111

1、可以当做一个 MySQL 数据库来使用

2、支持 MySQL 之外的数据库,通过 JDBC 实现

3、解决了我们提到的所有问题,多表 join、分布式事务、全局序列号、翻页排 序

4、支持 ZK 配置,带监控 mycat-web

5、2.0 正在开发

3.1.2 核心概念

image-20220307214027809

image-20220307214042173

image-20220307214215333

3.2 Mycat 配置详解

3.2.1 server.xml

包含系统配置信息。 system 标签:例如字符集、线程数、心跳、分布式事务开关等等。 user 标签:配置登录用户和权限。

image-20220307214445443

image-20220307214455532

3.2.2 schema.xml

schema 在 MySQL 里面跟数据库是等价的。 schema.xml 包括逻辑库、表、分片规则、分片节点和数据源,可以定义多个 schema。 这里面有三个主要的标签(table、dataNode、dataHost)

<table/>

image-20220307214707478

image-20220307214724110

<dataNode/>

image-20220307214738631

<dataHost/>

image-20220307214749689

writeType:读写分离的配置,决定 update、delete、insert 语句的负载。

image-20220307214820194

3.2.3 rule.xml

image-20220307214853002

3.2.4 ZK 配置

Mysql系列九:使用zookeeper管理远程Mycat配置文件、Mycat监控、Mycat数据迁移(扩容) - 小不点啊 - 博客园 (cnblogs.com)

Mycat 也支持 ZK 配置(用于管理配置和生成全局 ID),执行 bin 目录下 init_zk_data.sh,会自动将 zkconf 下的所有配置文件上传到 ZK(先拷贝到这个目录)

cd /usr/local/soft/mycat/conf
cp *.txt *.xml *.properties zkconf/
cd /usr/local/soft/mycat/bin
./init_zk_data.sh

image-20220307215341059

注意如果执行 init_zk_data.sh 脚本报错的话,代表未写入成功,此时不要启用 ZK 配置并重启,否则本地文件会被覆盖。

启动时如果 loadzk=true 启动时,会自动从 zk 下载配置文件覆盖本地配置。

在这种情况下如果修改配置,需要先修改 conf 目录的配置,copy 到 zkconf,再执 行上传。

3.2.5 启动停止

image-20220307215410654

3.3 Mycat 分片验证

在所配置的数据中建表

CREATE TABLE `customer` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `order_info` (
  `order_id` int(11) NOT NULL COMMENT '订单ID',
  `uid` int(11) DEFAULT NULL COMMENT '用户ID',
  `nums` int(11) DEFAULT NULL COMMENT '商品数量',
  `state` int(2) DEFAULT NULL COMMENT '订单状态',
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `order_detail` (
  `order_id` int(11) NOT NULL COMMENT '订单号',
  `id` int(11) NOT NULL COMMENT '订单详情',
  `goods_id` int(11) DEFAULT NULL COMMENT '货品ID',
  `price` decimal(10,2) DEFAULT NULL COMMENT '价格',
  `is_pay` int(2) DEFAULT NULL COMMENT '支付状态',
  `is_ship` int(2) DEFAULT NULL COMMENT '是否发货',
  `status` int(2) DEFAULT NULL COMMENT '订单详情状态',
  PRIMARY KEY (`order_id`,`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `student` (
  `sid` int(8) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `qq` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`sid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

schema.xml

<table name="customer" dataNode="dn1,dn2,dn3" rule="rang-long-cust" primaryKey="id"/>
<table name="order_info" dataNode="dn1,dn2,dn3" rule="mod-long-order">
<childTable name="order_detail" joinKey="order_id" parentKey="order_id" primaryKey="id"/>
</table>
<table name="student" dataNode="dn1,dn2,dn3" primaryKey="sid" type="global"></table>

数据节点配置

<dataNode name="dn1" dataHost="host1" database="gpcat"/>
<dataNode name="dn2" dataHost="host2" database="gpcat"/>
<dataNode name="dn3" dataHost="host3" database="gpcat"/>
<dataHost balance="0" maxCon="1000" minCon="10" name="host1" writeType="0" switchType="1"
	slaveThreshold="100" dbType="mysql" dbDriver="native">
	<heartbeat>select user()</heartbeat>
	<writeHost host="hostM1" url="192.168.8.146:3306" password="123456" user="root"/>
</dataHost>
<dataHost balance="0" maxCon="1000" minCon="10" name="host2" writeType="0" switchType="1"
    slaveThreshold="100" dbType="mysql" dbDriver="native">
    <heartbeat>select user()</heartbeat>
    <writeHost host="hostM1" url="192.168.8.150:3306" password="123456" user="root"/>
</dataHost>
<dataHost balance="0" maxCon="1000" minCon="10" name="host3" writeType="0" switchType="1"
    slaveThreshold="100" dbType="mysql" dbDriver="native">
    <heartbeat>select user()</heartbeat>
    <writeHost host="hostM1" url="192.168.8.151:3306" password="123456" user="root"/>
</dataHost>

3.3.1 范围分片

<tableRule name="rang-long-cust">
    <rule>
        <columns>id</columns>
        <algorithm>rang-long-cust</algorithm>
    </rule>
</tableRule>
<function name="rang-long-cust" class="io.mycat.route.function.AutoPartitionByLong">
	<property name="mapFile">rang-long-cust.txt</property>
</function>
INSERT INTO `customer` (`id`, `name`) VALUES (6666, '赵先生');
INSERT INTO `customer` (`id`, `name`) VALUES (7777, '钱先生');
INSERT INTO `customer` (`id`, `name`) VALUES (16666, '孙先生');
INSERT INTO `customer` (`id`, `name`) VALUES (17777, '李先生');
INSERT INTO `customer` (`id`, `name`) VALUES (26666, '周先生');
INSERT INTO `customer` (`id`, `name`) VALUES (27777, '吴先生');

3.3.2 取模分片(ER 表)

order_info
<tableRule name="mod-long-order">
    <rule>
        <columns>order_id</columns>
        <algorithm>mod-long</algorithm>
    </rule>
</tableRule>
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
	<property name="count">3</property>
</function>
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (1, 1000001, 1, 2, '2019-9-23 14:35:37', '2019-9-23 14:35:37');
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (2, 1000002, 1, 2, '2019-9-24 14:35:37', '2019-9-24 14:35:37');
INSERT INTO `order_info` (`order_id`, `uid`, `nums`, `state`, `create_time`, `update_time`) VALUES (3, 1000003, 3, 1, '2019-9-25 11:35:49', '2019-9-25 11:35:49');
order_detail
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (3, 20180001, 85114752, 19.99, 1, 1, 1);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (1, 20180002, 25411251, 1280.00, 1, 1, 0);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (1, 20180003, 62145412, 288.00, 1, 1, 2);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (2, 20180004, 21456985, 399.00, 1, 1, 2);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (2, 20180005, 21457452, 1680.00, 1, 1, 2);
INSERT INTO `order_detail` (`order_id`, `id`, `goods_id`, `price`, `is_pay`, `is_ship`, `status`) VALUES (2, 20180006, 65214789, 9999.00, 1, 1, 3);

3.3.3 全局表

student
<table name="student" dataNode="dn1,dn2,dn3" primaryKey="sid" type="global"/>
INSERT INTO `student` (`sid`, `name`, `qq`) VALUES (1, '黑白', '166669999');
INSERT INTO `student` (`sid`, `name`, `qq`) VALUES (2, 'AV 哥', '466669999');
INSERT INTO `student` (`sid`, `name`, `qq`) VALUES (3, '最强菜鸟', '368828888');
INSERT INTO `student` (`sid`, `name`, `qq`) VALUES (4, '加载中', '655556666');
INSERT INTO `student` (`sid`, `name`, `qq`) VALUES (5, '猫老公', '265286999');
INSERT INTO `student` (`sid`, `name`, `qq`) VALUES (6, '一个人的精彩', '516895555');

3.4 Mycat 全局ID

Mycat 全局序列实现方式主要有 4 种:本地文件方式、数据库方式、本地时间戳 算法、ZK。也可以自定义业务序列。

3.4.1 文件方式

配置文件 server.xml sequnceHandlerType 值: 0 文件 1 数据库 2 本地时间戳 3 ZK

<property name="sequnceHandlerType">0</property>

文件方式,配置 conf/sequence_conf.properties

CUSTOMER.HISIDS= CUSTOMER.MINID=10000001
CUSTOMER.MAXID=20000000
CUSTOMER.CURID=10000001

语法:select next value for MYCATSEQ_CUSTOMER

INSERT INTO `customer` (`id`, `name`) VALUES (next value for MYCATSEQ_CUSTOMER, 'qingsh')

优点:本地加载,读取速度较快。

缺点:当 Mycat 重新发布后,配置文件中的 sequence 需要替换。Mycat 不能 做集群部署。

3.4.2 数据库方式

<property name="sequnceHandlerType">1</property>

配置: sequence_db_conf.properties 把这张表创建在 146 上,所以是dn1

#sequence stored in datanode
GLOBAL=dn1
CUSTOMER=dn1

3.4.3 本地时间戳方式

ID= 64 位二进制 (42(毫秒)+5(机器 ID)+5(业务编码)+12(重复累加) ,长度为 18

<property name="sequnceHandlerType">2</property>

配置文件 sequence_time_conf.properties

#sequence depend on TIME
WORKID=01
DATAACENTERID=01

3.4.4 ZK 方式

修改 conf/myid.propertie

设置 loadZk=true(启动时会从 ZK 加载配置,一定要注意备份配置文件,并且先 用 bin/init_zk_data.sh,把配置文件写入到 ZK

<property name="sequnceHandlerType">3</property>

配置文件:sequence_distributed_conf.properties

# 代表使用 zk
INSTANCEID=ZK
# 与 myid.properties 中的 CLUSTERID 设置的值相同
CLUSTERID=010

复制配置文件

cd /usr/local/soft/mycat/conf
cp *.txt *.xml *.properties zkconf/
chown -R zkconf
cd /usr/local/soft/mycat/bin
./init_zk_data.sh

3.4.5 使用

4.Mycat 监控与日志查看

4.1 监控

4.1.1 命令行监控

4.1.2 命令行监控 mycatweb 监控

4.2 日志

4.2.1 wrapper.log 日志

4.2.2 mycat.log 日志