因为 SQL 语句是在我们的应用端编写的,所以第一步,我们可以在程序中对 SQL 语 句进行优化,最终的目标是用到索引。这个是容易的也是最常用的优化手段。
第二步,数据是存放在表里面的,表又是以不同的格式存放在存储引擎中的,所以 我们可以选用特定的存储引擎,或者对表进行分区,对表结构进行拆分或者冗余处理, 或者对表结构比如字段的定义进行优化。
第三步,对于数据库的服务,我们可以对它的架构进行优化。
如果只有一台数据库的服务器,我们可以运行多个实例,做集群的方案,做负载均衡。
或者基于主从复制实现读写分离,让写的服务都访问 master 服务器,读的请求都访 问从服务器,slave 服务器自动 master 主服务器同步数据。
或者在数据库前面加一层缓存,达到减少数据库的压力,提升访问速度的目的。
为了分散数据库服务的存储压力和访问压力,我们也可以把不同的数据分布到不同 的服务节点,这个就是分库分表(scale out)。
注意主从(replicate)和分片(shard)的区别: 主从通过数据冗余实现高可用,和实现读写分离。
分片通过拆分数据分散存储和访问压力。
第四步,是数据库配置的优化,比如连接数,缓冲区大小等等,优化配置的目的都 是为了更高效地利用硬件。
在分库之后,还需要进一步进行分表。当然,我们最开始想到的可能是在一 个数据库里面拆分数据,分区或者分表,到后面才是切分到多个数据库中。
分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。
垂直切分:基于表或字段划分,表结构不同。我们有单库的分表,也有多库的分库。
水平切分:基于数据划分,表结构相同,数据不同,也有同库的水平切分和多库的 切分。
垂直分表有两种,一种是单库的,一种是多库的。
单库分表,比如:商户信息表,拆分成基本信息表,联系方式表,结算信息表,附 件表等等。
多库垂直分表就是把原来存储在一个库的不同的表,拆分到不同的数据库。
水平切分也可以分成两种,一种是单库的,一种是多库的。
另一种是多库的水平分表。比如客户表,我们拆分到多个库存储,表结构是完全一 样的。
1、字段冗余
比如我们查询合同库的合同表的时候需要关联客户库的客户表,我们可以直接把一 些经常关联查询的客户字段放到合同表,通过这种方式避免跨库关联查询的问题。
2、数据同步:比如商户系统要查询产品系统的产品表,我们干脆在商户系统创建一 张产品表,通过 ETL 或者其他方式定时同步产品数据。
3、全局表(广播表) 比如行名行号信息被很多业务系统用到,如果我们放在核心 系统,每个系统都要去关联查询,这个时候我们可以在所有的数据库都存储相同的基础 数据。
4、ER 表(绑定表)
我们有些表的数据是存在逻辑的主外键关系的,比如订单表 order_info,存的是汇 总的商品数,商品金额;订单明细表 order_detail,是每个商品的价格,个数等等。或者 叫做从属关系,父表和子表的关系。他们之间会经常有关联查询的操作,如果父表的数 据和子表的数据分别存储在不同的数据库,跨库关联查询也比较麻烦。
5、系统层组装
在不同的数据库节点把符合条件数据的数据查询出来,然后重新组装,返回给客户 端。
CAP 三者是不能共有的,只能同时满足其中两点。基于 AP,我们又有了BASE理论。
基本可用(Basically Available):分布式系统在出现故障时,允许损失部分可用 功能,保证核心功能可用。
软状态(Soft state):允许系统中存在中间状态,这个状态不影响系统可用性, 这里指的是 CAP 中的不一致。
最终一致(Eventually consistent):最终一致是指经过一段时间后,所有节点数 据都将会达到一致。
分布式事务有几种常见的解决方案:
1、全局事务(比如 XA 两阶段提交;应用、事务管理器(TM)、资源管理器(DB)), 例如 Atomikos
2、基于可靠消息服务的分布式事务
3、柔性事务 TCC(Try-Confirm-Cancel)tcc-transaction
4、最大努力通知,通过消息中间件向其他系统发送消息(重复投递+定期校对)
我们有几种常见的解决方案:
1)UUID(Universally Unique Identifier 通用唯一识别码) UUID 标准形式包含 32 个 16 进制数字,分为 5 段,形式为 8-4-4-4-12 的 36 个字 符,例如:c4e7956c-03e7-472c-8909-d733803e79a9。
2)数据库
把序号维护在数据库的一张表中。这张表记录了全局主键的类型、位数、起始值,当前值。当其他应用需要获得全局 ID 时,先 for update 锁行,取到值+1 后并且更新后 返回。并发性比较差。
3)Redis 基于 Redis 的 INT 自增的特性,使用批量的方式降低数据库的写压力,每次获取一 段区间的 ID 号段,用完之后再去数据库获取,可以大大减轻数据库的压力。
4)雪花算法 Snowflake(64bit)
核心思想:
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 重复
我们先要分析一下 SQL 执行经过的流程。
DAO——Mapper(ORM)——JDBC——代理——数据库服务
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 和路由规则),可以灵活地定制。
缺点:不能复用,不能跨语言
第二个是在框架层,比如我们用 MyBatis 连接数据库,也可以指定数据源。我们可 以基于 MyBatis 插件的拦截机制(拦截 query 和 update 方法),实现数据源的选择。
不管是MyBatis还是Hibernate,还是Spring的JdbcTemplate,本质上都是对JDBC 的封装,所以第三层就是驱动层。比如 Sharding-JDBC,就是对 JDBC 的对象进行了封 装。JDBC 的核心对象:
DataSource:数据源
Connection:数据库连接
Statement:语句对象
ResultSet:结果集
前面三种都是在客户端实现的,也就是说不同的项目都要做同样的改动,不同的编 程语言也有不同的实现,所以我们能不能把这种选择数据源和实现路由的逻辑提取出来, 做成一个公共的服务给所有的客户端使用呢? 这个就是第四层,代理层。比如 Mycat 和 Sharding-Proxy,都是属于这一层
Mycat-doc/入门指南 at master · MyCATApache/Mycat-doc · GitHub
GitHub - MyCATApache/Mycat-Server
历史:从阿里 cobar 升级而来,由开源组织维护,2.0 正在开发中。
定位:运行在应用和数据库之间,可以当做一个 MySQL 服务器使用,实现对 MySQL 数据库的分库分表,也可以通过 JDBC 支持其他的数据
Mycat 的关键特性(官网首页)
1、可以当做一个 MySQL 数据库来使用
2、支持 MySQL 之外的数据库,通过 JDBC 实现
3、解决了我们提到的所有问题,多表 join、分布式事务、全局序列号、翻页排 序
4、支持 ZK 配置,带监控 mycat-web
5、2.0 正在开发
包含系统配置信息。 system 标签:例如字符集、线程数、心跳、分布式事务开关等等。 user 标签:配置登录用户和权限。
schema 在 MySQL 里面跟数据库是等价的。 schema.xml 包括逻辑库、表、分片规则、分片节点和数据源,可以定义多个 schema。 这里面有三个主要的标签(table、dataNode、dataHost)
writeType:读写分离的配置,决定 update、delete、insert 语句的负载。
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
注意如果执行 init_zk_data.sh 脚本报错的话,代表未写入成功,此时不要启用 ZK 配置并重启,否则本地文件会被覆盖。
启动时如果 loadzk=true 启动时,会自动从 zk 下载配置文件覆盖本地配置。
在这种情况下如果修改配置,需要先修改 conf 目录的配置,copy 到 zkconf,再执 行上传。
在所配置的数据中建表
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>
<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, '吴先生');
<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');
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);
<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');
Mycat 全局序列实现方式主要有 4 种:本地文件方式、数据库方式、本地时间戳 算法、ZK。也可以自定义业务序列。
配置文件 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 不能 做集群部署。
<property name="sequnceHandlerType">1</property>
配置: sequence_db_conf.properties 把这张表创建在 146 上,所以是dn1
#sequence stored in datanode
GLOBAL=dn1
CUSTOMER=dn1
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
修改 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