MySql

1、数据库范式

第一范式:数据库表中的属性是原子性的,比如地址中的省、市、区、街道等如果拆分成多个小字段就是符合第一范式的,如果地址是一个字段,就不满足第一范式;

第二范式:数据库中的每个实例或记录必须可以被唯一区分,说白了就是要有主键,其他的字段都依赖于主键。

第三范式:任何非主属性不依赖于其他非主属性,也就是说,非主键外的字段必须互不依赖。

在遵守范式的数据中,表中是不能有任何冗余字段的,这使得查询的时候就经常会需要多表联查,这无疑比较耗时,所以就有了反范式化。比如常见的增加冗余字段以避免多表join,就是"用空间换时间"以提高查询效率,需要注意的是冗余字段的一致性。

2、sql语句的执行过程

(1)客户端/服务器端通信协议于MySql建立连接,并查询是否有权限。

(2)检查是否开启缓存,开启了Query Cache且命中完全相同的SQL语句则将查询结果直接返回给客户端。

(3)解析器进行语法分析和语义分析生成解析树,预处理器检查解析树是否合法,比如表、字段是否存在等。

(4)优化器生成执行计划,根据索引看看是否可以优化。

(5)执行器执行sql,若开启了Query Cache则缓存否则直接返回。

3、数据库事务机制

ACID特性

数据库事务就是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。它具有4个属性:

原子性:事务作为一个整体被执行,其中对数据库的操作要么全部执行,要么全部不执行。

一致性:事务应确保数据库的状态从一个一致状态到另一个一致状态。

隔离性:多个事务并发执行,一个事务的执行不影响其他事务的执行。

持久性:事务一旦提交,它对数据库的修改应该永久保存在数据库中。

MySql事务隔离级别

先了解下脏读、不可重复读和幻读问题:

脏读:读到了其他事务还没提交的数据。

不可重复读:对某数据进行读取时,有其他事务对数据进行了修改。导致第二次读取的结果不同。

幻读:事务在做范围查询时,有另外一个事务对范围内新增了记录,导致范围查询的结果的条数不一致。

隔离级别从低到高分别为:

未提交读(RU):一个事务可以读到另一个事务未提交的数据。这种隔离级别可能导致幻读、不可重复读和脏读。

已提交读(RC):一个事务修改数据过程中,如果事务还没提交,其它事务不能读该数据。这种隔离级别可以避免脏读的问题。

可重复读(RR):可以解决不可重复读的问题,但是不能彻底解决幻读的问题。

可串行化:最高隔离级别,可解决幻读的问题,但是数据库执行效率变低。

Oracle默认是提交读(RC):Oracle总共就支持三种,另两种时串行化和只读,很明显不合适做默认隔离级别。

MySql默认是可重复读(RR),MySql的隔离级别中RU级别太低,串行化级别太高,所以默认可选择就在RC和RR中。我们知道MySql是采用主从复制机制来避免单点故障问题的,所谓主从复制,就是搭建MySql集群,整体对外提供服务,集群中主服务器提供写服务,从服务器提供读服务。为了保证主从库数据一致性,需要进行数据同步。数据同步是通过bin log进行的,主服务器把数据变更记录到bin log,然后再把bin log 同步传输给从服务器,从服务器接收到bin log后把其中的数据恢复到自己的数据库存储中。 MySql的bin log主要支持三种格式:statement、row以及mixed,早期其实只有statement格式,这种格式下,bin log中记录的是SQL语句的原文。如果现在有两个事务:一个是删除条件数据,一个是插入一条数据,在RC隔离级别下,删除事务在执行完但是未提交时插入事务执行完并提交,最后删除事务提交,这样在主库中是会只有一条刚插入的数据,但是由于bin log记录的是SQL原文,这是从库拿到的执行语句就是先插后删,从库就没有数据,这就导致主备不一致。而采用RR隔离级别,删除事务执行时会增加GAP锁(间隙锁)和临键锁,这样插入事务会被阻塞,等到删除事务提交或回滚后才开始执行,就能保证bin log 中记录顺序正确的sql。

所以MySql默认是可重复读(RR)是为了兼容历史上statement格式的bin log,而且如果MySql使用statement格式的bin log的话,如果主动修改隔离级别为RC是会报错的。

RR和RC的区别:

(1)快照读

RR中,快照会在事务第一次select时生成,后续查询都是使用这一个快照,只有在本事务中对数据进行更改才会更新快照。

RC中每次读取都会重新生成一个快照,总是读取行的最新版本。RC还支持半一致读。

(2)锁机制

MySql中有三种锁:

Record Lock:记录锁,锁的是索引记录。

Gap Lock:间隙锁,锁的是索引记录之间的间隙。

Next-Key Lock:Record Lock和Gap Lock的组合,同时锁索引记录和间隙,范围是左开右闭。

RC中只会对索引增加Record Lock,不会有Gap Lock和Next-Key Lock。

RR为了解决幻读的问题,除了Record Lock,也支持Gap Lock和Next-Key Lock。

(3)主从同步:

RC隔离级别支持ROW格式的,RR支持三种格式的bin log。

为什么互联网公司选择RC:

其实于两种隔离级别下的锁机制有关,RC在加锁的过程中只有Record Lock(记录锁),而不会添加间隙锁和临键锁,这就使得其并发能力更突出,而互联网公司注重的就是高并发业务。另外更多的锁也就意味着发生死锁的概率也就更高,至于不可重复读的问题也可以通过其他手段解决,所以平衡考虑下来这些公司就会选择用RC而不是RR。

Innodb的RR到底有没有解决幻读?

首先,Innodb的RR这种隔离进别通过间隙锁+MVCC解决了大部分的幻读问题,但并不是所有幻读都能解决,想要彻底解决幻读,还是需要使用串行化隔离级别。

RR中,通过间隙锁解决了部分当前读的幻读问题,增加间隙锁将记录之间的间隙锁住,避免新数据的插入。

RR中,通过MVCC机制解决了快照读的幻读问题,RR中的快照读只有第一次会查询数据,后面都是直接读取快照数据,所以不会发生幻读。

但是如果两个事务,事务1先进行快照读,然后事务2插入了一条记录并提交,再在事务1更新信插入的这条记录是可以成功的,这就是发生了幻读。

还有事务1先进行快照读,然后事务2插入了一条数据并提交,在事务1中进行当前读之后,再进行快照读也会发生幻读。

MySql事务2阶段提交

MySql事务的提交是分2阶段提交以保证binlog和redolog的一致性,避免产生主备库数据不一致问题。

Prepare阶段:SQL成功执行并生成redo log,处于prepare阶段。

BinLog持久化:bin log 提交,write()将binlog内存日志数据写入文件缓冲区,fsunc()将binlog从文件缓冲区永久写入磁盘。

Commit提交:在执行引擎内部执行事务操作,更新redolog,处于Commit阶段。

4、InnoDB的锁机制

按锁的粒度:全局锁、表级锁、行级锁

按锁的级别:共享锁(读锁)、排他锁(写锁)

当数据加了排他锁,其他事务不能在这一行上加任何锁,获得锁的事务既能读也能写,slect ... for update。

当数据加了共享锁,其他事务依然可以对这行数据加共享锁,但不能加排他锁,获得共享锁的事务只能读不能写,select ... lock in share mode。

按使用方式:乐观锁、悲观锁

悲观锁:认为并发修改的概率很高,所以修改数据时直接对数据加锁。关闭MySql数据库的自动提交属性,然后通过select ... for update来进行加锁。

乐观锁:认为数据一般情况下不会造成冲突,所以在更新提交时才进行校验,一般通过增加版本号进行更新校验。不直接使用锁不代表乐观锁没有锁,Update的过程是有锁的,根据where条件对索引增加行级锁。

按锁的对象:记录锁、间隙锁、临键锁

Record Lock:记录锁,锁的是索引记录。

Gap Lock:间隙锁,锁的是索引记录之间的间隙。

Next-Key Lock:Record Lock和Gap Lock的组合,同时锁索引记录和间隙,范围是左开右闭。

InnoDB的RR级别中,加锁的基本单位是Next-Key Lock,只要扫描到的数据都会加锁,唯一索引上的范围查询会访问到不满足条件的第一个值为止。

有两个优化点:

(1)索引上的等值查询,给唯一索引加锁的时候,Next-Key Lock退化成行锁。

(2)索引上的等值查询,向右遍历时且最后一个值不满足等值条件时,Next-Key Lock退化成间隙锁。

比较特殊的还有AUTO-INC锁,一种表级锁,能够保证自增主键连续不重复。

补充:MySql 5.6之前,InnoDB索引构建期间会对表进行排他锁定,其他会话不能读取或修改表中的任何数据,MySql 5.6之后,InnoDB使用Online DDL技术,允许在不阻塞其他会话的情况下创建或删除索引,针对不同的操作有不同的实现方式,如COPY、INSTANT、INPLACE等。

5、InnoDB中的索引

InnoDB中常见的索引数据结构:B+树索引和Hah索引

B+树索引分为聚簇索引和非聚簇索引 。聚簇索引就是按照每张表的主键构造一个B+树,树的非叶子节点上是索引值,叶子节点中记录着表中一行记录的所有值。非聚簇索引的叶节点中不包含行记录的所有值,只包含索引值和主键值。使用非聚簇索引查询需要进行一次回表,就是先找到主键,再用主键去数据库查询所需字段。

利用覆盖索引索引下推也能一定程度上减少回表的发生:

覆盖索引:查询语句的执行只需要从索引中就能够获得,不必从数据表中读取。比如联合索引,在保证最左前缀原则的查询时,查询信息是索引中的某个字段。

索引下推:MySql 5.6引入的新优化,还是联合索引,如果某个非前导列因为索引失效而要进行扫表回表时就会触发索引下推优化,比如 select d from t where a = "" and b = 1 , a、b是联合索引,都是varchar类型,由于b的类型不匹配导致索引失效,因为有索引下推,实际还是可以减少回表的次数的(官方以like模糊查询%在前导致索引失效说明索引下推优化)。

唯一索引和主键索引:

通过事务和锁机制保证唯一性:一个事务修改索引列时其他事务会被阻塞。写入数据导磁盘时,MySql也会进行约束检查,确保不会违反唯一性约束。

唯一索引允许Null值,而且可以是多个Null值,因为在每个Null值都被认为是"未知"的,所以是不一样的。缺点就是对插入性能有一定影响,另外如果要更新唯一性索引列的值需要先删除记录再插入新记录。

为什么使用B+树实现索引:

支持范围查询和排序(索引值按大小排序后存储的);

非叶子节点不存储实际数据,只存储索引关键字,可以支持更多的索引数据;

由于叶子节点大小是固定的,而且节点的大小一般都会设置成一页的大小,所以节点分裂和合并及磁盘预读时可以一次性读取,IO操作少。

6、SQL优化

6.1 索引失效

针对慢sql,通过explain查看执行计划,注意type、key和extra字段,分别是索引类型,使用的索引和查询时的附加操作,三者结合来看可以看出是否使用了索引,如果有走索引,判断是否走了覆盖索引或者是否全扫描索引树等等。简单来说,key要有值,不能是NULL,type应该是ref、eq_ref、range、const等这几个,extrausing index、using index condition都是可以的。

失效原因:

(1)条件字段没有索引或者不满足最左前缀匹配

(2)索引区分度不高,可能会不走索引

(3)表数据少,直接全表扫描

(4)查询语句中,索引字段用了函数计算或者数据类型不一致等

上述(4)包含sql语句的问题补充如下:

(a)MySql用了函数计算之后索引不是一定会失效,MySql 8.之后引入函数索引。

(b)sql语句使用OR,并且OR两边使用<或>,如果OR两边使用的=的话还是可以走索引的。

(c)sql语句使用LIKE,%在 字符串的首位则不走索引。

(d)隐式类型转换,varchar类型字段查询时使用int类型,索引失效,但是注意int类型字段查询时加了单引号或者双引号,参数会自动转换为int类型,能走索引。

(e)sql语句使用 != ,并不是绝对不走索引,比如用自增主键ID时就可能走索引,这个要看索引的选择和数据分布情况。

(f)sql语句使用 is nt null。

(g)sql语句使用 order by,数据量很小时,直接在内存中排序,不使用索引。

(h)sql语句使用 in, in的值比较多的时候可能就不走索引 了。

6.2 多表join或者查询字段太多

MySql的嵌套查询效率较低,如果不用join可以考虑代码作二次查询再进行数据关联处理,或者设计表的时候允许数据冗余或基于join关系做宽表。

6.3 数据库连接数不够

常见于热点数据更新,多个update语句会排队获取锁,占用连接资源 ,解决思路有1、基于缓存做数据更新,如redis,2、异步更新或者批量更新。

另外就是存在长事务,占用了连接导致其他请求要等待。

6.4 数据库IO或者CPU比较高

常见于并发修改数据引起的锁等待,现象就是CPU彪高的同时可以看到大量SQL的锁耗时比较长。

6.5 深度分页问题

考虑使用子查询以及记录上一页ID的方案