1 设计优化
1.1 字段设计
1.尽量使用整型表示字符串。
2.decimal不会损失精度,存储空间会随数据的增大而增大。double占用固定空间,较大数的存储会损失精度。
3.尽可能选择小的数据类型和指定短的长度。
4.尽可能使用 not null。
5.单表字段不宜过多。
1.2 数据库设计三范式
-
第一范式(1NF):要求数据库表的每一列都是不可分割的原子数据项。
-
第二范式(2NF):在1NF的基础上,非码属性必须完全依赖于候选码(在1NF基础上消除非主属性对主码的部分函数依赖)需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。
-
第三范式(3NF):在2NF基础上,任何非主属性不依赖于其它非主属性(在2NF基础上消除传递依赖)需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
1.3 存储引擎选择
1.3.1 存储差异
MyISAM | Innodb | |
---|---|---|
文件格式 | 数据.MYD和索引.MYI分别存储 | 数据和索引集中存储.ibd |
文件能否移动 | 能,一张表对应.frm、MYD、MYI文件 | 否,关联的还有data下文件 |
记录存储顺序 | 按记录插入顺序保存 | 按主键大小有序插入 |
空间碎片 | 产生 | 不产生 |
事务 | 不支持 | 支持 |
外健 | 不支持 | 支持 |
锁支持 | 表级锁定 | 行级锁定,表级锁定 |
1.3.2 选择依据
MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键保证数据完整性。比如OA自动化办公系统。
2 索引优化
2.1 概念
关键字与数据的映射关系称为索引(包含关键字和对应的记录在磁盘中的地址)。
2.2 索引类型
-
普通索引:对关键字没有限制
-
唯一索引:要求记录提供的关键字不能重复
-
主键索引:要求关键字唯一且不为null
2.3 索引的存储结构
-
BTree
多路查找平衡树,BTree的一个node可以存储多个关键字,node的大小取决于计算机的文件系统,因此我们可以通过减小索引字段的长度使结点存储更多的关键字。
-
B+Tree聚簇结构
在MySQL中,仅仅只有
Innodb
的主键索引为聚簇结构,其它的索引包括Innodb
的非主键索引都是典型的BTree结构。 -
哈希索引
在索引被载入内存时,使用哈希结构来存储。
2.4 创建索引
-
创建表之后建立索引
-- 更改表结构alter table user_index-- 创建一个first_name和last_name的复合索引,并命名为nameadd key name (first_name,last_name),-- 创建一个id_card的唯一索引,默认以字段名作为索引名add UNIQUE KEY (id_card),-- 鸡肋,全文索引不支持中文add FULLTEXT KEY (information);
-
创建表时指定索引
CREATE TABLE user_index2 ( id INT auto_increment PRIMARY KEY, first_name VARCHAR (16), last_name VARCHAR (16), id_card VARCHAR (18), information text, KEY name (first_name, last_name), FULLTEXT KEY (information), UNIQUE KEY (id_card));
2.5 索引优化
可以通过explain selelct
来分析SQL语句执行前的执行计划
-
建立基础索引:在
where、order by、join
字段上建立索引。 -
优化,组合索引:基于业务逻辑
-
如果条件经常性出现在一起,那么可以考虑将多字段索引升级为复合索引
-
如果通过增加个别字段的索引,就可以出现索引覆盖,那么可以考虑为该字段建立索引
-
查询时,不常用到的索引,应该删除掉
简要解释下explain各个字段的含义
-
id : 表示SQL执行的顺序的标识,SQL从大到小的执行
-
select_type:表示查询中每个select子句的类型
-
table:显示这一行的数据是关于哪张表的,有时不是真实的表名字
-
type:表示MySQL在表中找到所需行的方式,又称“访问类型”。常用的类型有:ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好)
-
possible_keys:指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用
-
Key:key列显示MySQL实际决定使用的键(索引),如果没有选择索引,键是NULL。
-
key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的)
-
ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
-
rows:表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数,理论上行数越少,查询性能越好
-
Extra:该列包含MySQL解决查询的详细信息
2.6 索引失效的场景
1.当语句中带有or的时候 即使有索引也会失效。
select * from USER where name='xzz' or age=16;
2.当语句索引 like %在前面的时候索引失效(注意:如果上句为 like ‘some%’此时索引是生效的)。
select * from USER where name like '%xzz' ;
3.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。
select * from USER where name=123;
4.mysql联合索引有最左原则,需要按照顺序。将name和age设置为联合索引。
select * from USER where age=11 and name='xzz';
5.在索引上进行计算操作。
select * from USER where age-1>11;
6.负向查询不能使用索引
select name from user where id not in (1,3,4);
3 SQL语句优化
3.1 sql优化
-
数据库导入大量数据时,可以先禁用索引和约束
alter table table-name disable keys
,导入后开启alter table table-name enable keys
。 -
limit offset,rows 尽量保证不要出现大的
offset
,比如limit 10000,10
相当于对已查询出来的行数弃掉前10000
行后再取10
行。 -
order by 不要用rand(),效率低。在应用程序中,将随机的主键生成好,去数据库中利用主键检索。
-
select() count()少用*,如果确定只查询一条数据,可以用limit 1。
3.2 慢查询日志
用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。
3.2.1 开启慢查询日志
配置项:slow_query_log
可以使用show variables like ‘slov_query_log’
查看是否开启,如果状态值为OFF
,可以使用set GLOBAL slow_query_log = on
来开启,它会在datadir
下产生一个xxx-slow.log
的文件。
3.2.2 设置临界时间
配置项:long_query_time
查看:show VARIABLES like 'long_query_time'
,单位秒
设置:set long_query_time=0.5
实操时应该从长时间设置到短的时间,即将最慢的SQL优化掉
3.2.3 查看日志
一旦SQL超过了我们设置的临界时间就会被记录到xxx-slow.log
中
3.3 profile信息
3.3.1 查看状态,并开启
show variables like 'profiling';--查看状态
set profiling=on;--开启
show profiles;--查看profile信息
3.3.2 通过ID查看某条sql所有详细步骤的时间
通过show profiles;
查找到ID,使用show profile for query Query_ID
查询经过了哪些步骤,各消耗了多场时间。
4 横向扩展(MySQL集群、负载均衡、读写分离)
4.1 数据库分区
当数据量较大时(一般千万条记录级别以上),MySQL的性能就会开始下降,这时我们就需要将数据分散到多组存储文件,保证其单个文件的执行效率。
4.1.1 分区管理语法
-
range/list
alter table article_range add partition( partition p201811 values less than (1543593599) -- select UNIX_TIMESTAMP('2018-11-30 23:59:59') -- more ); alter table article_range drop PARTITION p201808;--删除分区后,分区中原有的数据也会随之删除!
-
key/hash
alter table article_key add partition partitions 4;--新增 alter table article_key coalesce partition 6;--删除不会删除数据
4.1.2 mysql分区算法
-
hash(field)
相同的输入得到相同的输出。输出的结果跟输入是否具有规律无关。仅适用于整型字段。
create table article( id int auto_increment PRIMARY KEY, title varchar(64), content text )PARTITION by HASH(id) PARTITIONS 10;
-
key(field)
和
hash(field)
的性质一样,只不过key
是处理字符串的,比hash()
多了一步从字符串中计算出一个整型在做取模操作。create table article_key( id int auto_increment, title varchar(64), content text, PRIMARY KEY (id,title) -- 要求分区依据字段必须是主键的一部分 )PARTITION by KEY(title) PARTITIONS 10;
-
range算法
是一种条件分区算法,按照数据大小范围分区(将数据使用某种条件,分散到不同的分区中)。
create table article_range( id int auto_increment, title varchar(64), content text, created_time int, -- 发布时间到1970-1-1的毫秒数 PRIMARY KEY (id,created_time) -- 要求分区依据字段必须是主键的一部分 )charset=utf8PARTITION BY RANGE(created_time)( PARTITION p201808 VALUES less than (1535731199), -- select UNIX_TIMESTAMP('2018-8-31 23:59:59') PARTITION p201809 VALUES less than (1538323199), -- 2018-9-30 23:59:59 PARTITION p201810 VALUES less than (1541001599) -- 2018-10-31 23:59:59 );
-
list算法
也是一种条件分区,按照列表值分区(
in (值列表)
)。create table article_list( id int auto_increment, title varchar(64), content text, status TINYINT(1),-- 文章状态:0-草稿,1-完成但未发布,2-已发布 PRIMARY KEY (id,status) -- 要求分区依据字段必须是主键的一部分 )charset=utf8PARTITION BY list(status)( PARTITION writing values in(0,1), -- 未发布的放在一个分区 PARTITION published values in (2) -- 已发布的放在一个分区 ); insert into article_list values(null,'mysql优化','内容示例',0); flush tables;
4.2 大表分表
-
垂直分表: 根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
-
垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
-
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
-
-
水平分表: 保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。 数据库分片的两种常见方案:
-
客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
-
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
-
4.3 集群
单中心双机的常见方案有以下这些:
-
一主一备的架构(主备式)
将数据库部署到两台机器,其中一台机器(代号A)作为日常提供数据读写服务的机器,称为「主机」。另外一台机器(代号B)并不提供线上服务,但会实时的将「主机」的数据同步过来,称为「备机」。一旦「主机」出了故障,通过人工的方式,手动的将「主机」踢下线,将「备机」改为「主机」来继续提供服务。
缺点:人工干预。
-
一主一从的架构(主从式)
主从式架构中的「从机」虽然也在随时随刻提供服务,但是它只提供「读」服务,并不提供「写」服务。「主机」会实时的将线上数据同步到「从机」,以保证「从机」能够正常的提供读操作。
主从双机自动切换: 当主机出现故障后,从机能够自动检测发现。同时从机将自己迅速切换为主机,将原来的主机立即下线服务,或转换为从机状态。
缺点:数据同步延迟。
-
互为主从的架构(主主式)
互为主从的架构是指两台机器自己都是主机,并且也都是作为对方的从机。两台机器都提供完整的读写服务,因此无需切换,客户机在调用的时候随机挑选一台即可,当其中一台宕机了,另外一台还可以继续服务。
缺点:双向复制,数据延迟或丢失。
4.4 读写分离
读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。因为主从复制要求slave
不能写只能读(如果对slave
执行写操作,那么show slave status
将会呈现Slave_SQL_Running=NO
,此时你需要按照前面提到的手动同步一下slave
)。
4.4.1 定义两种连接
就像我们在学JDBC时定义的DataBase
一样,我们可以抽取出ReadDataBase,WriteDataBase implements DataBase
,但是这种方式无法利用优秀的线程池技术如DruidDataSource
帮我们管理连接,也无法利用Spring AOP
让连接对DAO
层透明。
4.4.2 使用Spring AOP
在调用DAO接口时根据接口方法命名规范(增addXXX/createXXX
、删deleteXX/removeXXX
、改updateXXXX
、查selectXX/findXXX/getXX/queryXXX
)动态地选择数据源(读数据源对应连接master
而写数据源对应连接slave
),那么就可以做到读写分离了。(可以参考:Spring 动态切换、添加数据源实现以及源码浅析)
4.5 负载均衡
4.5.1 负载均衡算法
-
轮询
-
加权轮询:按照处理能力来加权
-
负载分配:依据当前的空闲状态(但是测试每个节点的内存使用率、CPU利用率等,再做比较选出最闲的那个,效率太低)
4.5.2 高可用
在服务器架构时,为了保证服务器7x24不宕机在线状态,需要为每台单点服务器(由一台服务器提供服务的服务器,如写服务器、数据库中间件)提供冗余机。(推荐:mysql+mycat搭建稳定高可用集群)
对于写服务器来说,需要提供一台同样的写-冗余服务器,当写服务器健康时(写-冗余通过心跳检测),写-冗余作为一个从机的角色复制写服务器的内容与其做一个同步;当写服务器宕机时,写-冗余服务器便顶上来作为写服务器继续提供服务。对外界来说这个处理过程是透明的,即外界仅通过一个IP访问服务。