MySQL篇
前言
MySQL是一种关系型数据库,有着开源免费,方便拓展的优势。本篇文章主要对MySQL的面试相关知识点做一个总结,主要包括以下内容:存储引擎、字符集和校对规则、索引、查询缓存的使用、事务、锁机制、大表优化、数据库连接池等。
存储引擎
MySQL目前的默认存储引擎是InnoDB,并且在5.7版本中只有InnoDB支持事务。
在5.5版本之前,MySQL的默认引擎是MyISAM。
InnoDB相对于MyISAM的主要优势在于:
- InnoDB支持事务
- InnoDB支持外键
- InnoDB支持行锁和表锁,但是MyISAM只支持表锁(表锁指的是更新操作的时候会锁住整张表)。
- InnoDB支持崩溃修复功能。
- InnoDB支持MVCC(Multiversion Concurrency Control,多并发版本控制)。
而MyISAM的优势在于读操作的速度比较快。
字符集及校对规则
字符集
字符集指的是多个字符的集合,通过一个或者多个字节表示字符集中的一个字符。
常见的字符集包括:GBK、UTF8、ASCII等。
MySQL的字符集
MySQL有多个系统变量值与字符集相关:
- character_set_sever:默认的内部操作字符集
- character_set_client:客户端来源数据使用的字符集
- character_set_connection:连接层字符集
- character_set_results:查询结果字符集
- character_set_datebase:当前选中数据库的默认字符集
- character_set_system:系统元数据(字段名等)字符集
建立库、表。列字符集的时候:
- 建库时,如果没有指定字符集,则采用character_set_server指定的字符集。
- 建表时,如果没有指定字符集,则默认采用当前库使用的字符集。
- 新增时,修改表字段的时候如果没有明确指定字符集,则采用当前表的字符集。
当涉及更新和查询的时候:
- 更新的时候:MySQL会先向客户端询问使用什么字符集,客户端使用character_set_client参数进行告知,然后MySQL会查看是否和自己的character_set_connection参数一致,如果不一致会先进行一次转化。更新的时候会再次查看连接字符集是否和内部存储的字符集(表、字段等)一致,如果不一致还需要进行一次转换。
- 查询的时候:查询结果会转化为character_set_results所指定的字符集(character_set_results默认和character_set_client一致)
校对规则
校对规则指的是在字符集内部比较和排序的一套规则,有的规则区分大小写,有的则无视。
校对规则有以下特征:
- 两个不同的字符集不能有相同的校对规则。
- 每个字符集有一个默认的校对规则。
- 存在校对规则命名约定:以其相关的字符集名开始,中间包括一个语言名,并且以_ci(大小写不敏感)、_cs(大小写敏感)、_bin(二元)结束。
索引
MySQL使用的索引主要有BTree索引和哈希索引。在绝大多数需求为单条记录查询的时候可以选择哈希索引,性能较高,但是哈希索引的缺点在于无法进行高效范围查找,所以其他时候建议采用BTree索引。
什么是索引
索引的本质实际上是一张表,该表中保存了主键和索引字段,并且指向实体表的记录。
当执行创建表和索引语句之后,不同的引擎会产生不同的文件。
InnoDB会产生一个 *.frm 文件和 *.idb 文件,其中frm文件保存了创建表的语句,idb文件中保存了表中的数据和索引。这说明了InnoDB采用的是聚集索引,即索引中保存了具体的数据。
而MyISAM引擎会生成除了frm文件之外还会生成MYD(MyISAM Data)和MYI(MyISAM Index)文件,这说明MyISAM采用的是非聚集索引,索引中保存的是数据的物理地址。
事实上,对于InnoDB来说,可以同时支持聚集索引和非聚集索引:
- 聚集索引:如果表设置了主键,则主键就是聚簇索引如果表没有主键,则会默认第一个NOT NULL,且唯一(UNIQUE)的列作为聚簇索引,以上都没有,则会默认创建一个隐藏的row_id作为聚簇索引。
- 普通索引:使用普通键建立的索引,不存储具体数据,叶子节点上存放的是主键,所以在InnoDB中使用普通索引查询之后还要会回表进行一次主键查询(这样做的目的是为了节约空间)。
什么时候不会回表
并非每次使用普通索引都会进行回表,如果查询的数据就是使用普通索引的数据而非全部数据,那么不需要进行回表能够返回数据。
那么当使用联合索引(使用多个列建立索引)的时候,如果需要查询的数据就在列中且满足最左前缀匹配,那么同样也不需要进行回表。
当通过索引就可以返回查询所需要的数据的时候,可以称之为实现了索引覆盖。
联合索引
联合索引指的是多个键共同组成的索引,它遵循最左前缀匹配原则。
最左前缀匹配
假设创建了一个联合索引(key1,key2,key3),那么MySQL会根据最左优先的原则从最左边开始匹配,相当于建立(key1)、(key1,key2)、(key1,key2,key3)三个索引,但是(key2,key3)不满足条件。
所以一般把识别度最高的一个键放在最左边。
但是,联合索引与where条件无关,如果是where key3='1',key2='2',key1='3'
,依旧能够使用(key1,key2,key3)的联合索引,这是由于Mysql会对sql语句进行自动优化。
除此之外,当联合索引的某一个键使用了范围查询的时候,后面的键就会失效。
比如select * from myTest where key1>1 and key2=2 and key3 = 3
。在这个查询语句中,由于key1使用了范围查询,所以后面的key2和key3会失效。
select * from myTest where key1=1 order by key2
,在这条sql语句中,还是能够使用到联合索引的,key1下面任意一段key2都是排序好的。
索引失效
当MySQL优化器判断扫描全表要比通过索引查询快的时候,就不会通过索引进行查询。
除此之外还包括:
- 条件中带有or关键字
- like查询以%开头
- 在索引中进行计算、函数等。
- 隐式数据类型转换,比如id是varchar类型的,但是查询的时候使用了
id = 10000
而非id = "10000"
也会导致索引失效。 - 隐式字符编码转换,比如
where table1.id = table2.t1_id
但是t1和t2的字符编码不相同,也会导致索引失效。
唯一索引和普通索引
在说唯一索引和普通索引之前,首先要说一下change buffer,change buffer的功能其实和redo log有一些相似。
Mysql和操作系统类似,读取也是把一页数据调入内存,但是为了提高性能,Mysql会采用change buffer的功能,先把修改记录在change buffer中,等到下一次要读取这一行数据的时候再读入对应页,这个时候把change buffer中的操作刷新到内存页上。
当然,不把页读入内存就记录修改的话存在一个很大问题就是无法校验修改的有效性。
采用唯一索引的话,需要保证数据的唯一性,还是需要把数据读入内存中查看是否有相同的值,如果有的话就抛出异常,所以唯一索引是无法使用change buffer的。
查询缓存
MySQL8.0之前可以设置开启缓存,开启之后每次查询的时候会先从缓存中进行查询,如果在缓存中则直接返回。
事务
事务的定义
事务指的是逻辑上的一组操作,要么都执行,要么都不执行。
事务的四大特性
- 原子性:事务是最小的执行单位,不允许分割。
- 一致性:执行事务前后,数据保持一致,多个事务对同一个数据的读取的结果是相同的。
- 隔离性:并发访问数据库的时候,一个用户的事务不被其他事务干扰。
- 持久性:一个事务被提交之后,其对数据改变是永久的。
并发事务
并发事务会带来如下问题:
- 脏读:当一个事务正在访问数据并且对数据进行了修改,这个修改还没有提交到数据库中,另外一个事务也访问了这个数据,这时候读到的数据就是脏数据。
- 修改丢失:指一个事务正在对数据进行修改,另一个事务也对这个数据进行修改,可能会导致前一个事务的修改结果丢失。
- 不可重复读:指一个事务对数据进行多次读取,但是同一时间存在另一个事务进行修改,导致第一个事务多次读取的结果不一致。
- 幻读:只一个事务在读取多行记录,另一个事务对记录进行增删操作,导致第一个事务发现自己读取记录多了或者少了几行。
事务的隔离级别
SQL把隔离级别划分为4种:
- (READ-UNCOMMITTED)读取未提交:最低的隔离级别,允许读取未提交的数据变更,可能导致脏读、幻读或不可重复读。
- (READ-COMMITTED)读取已提交:允许读取已提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- (REPEATABLE-READ)可重复读:多次读取的结果是一致的,可以阻止脏读和不可重复读,但是幻读仍有可能发生。
- (SERIALIZABLE)可串行化:最高级别的隔离级别。
InnoDB存储引擎默认支持的隔离级别是可重复读,但是InnoDB在事务隔离级别下上的是Next-Key Lock锁算法,因此可以避免幻读的发生。
锁机制和InnoDB锁算法
MyISAM和InnoDB存储引擎使用的锁:
- MyISAM采用表级锁。
- InnoDM支持行级锁和表级锁,默认为行级锁。
表级锁和行级锁的区别:
- 表级锁:MySQL中颗粒度最大的一种锁,对当前操作的整张表进行加锁,实现简单,资源消耗少,加锁快,不会死锁。
- 行级锁:MySQL中颗粒度最小的一种锁,对当前的操作行进行加锁,行级锁能大大减少数据库操作的冲突。但是开销大、加锁慢、会死锁。
InnoDB的行级锁包括以下几种:
- Record Lock:对索引进行加锁,锁定符合条件的行。其他事务不能修改和删除该项。
- Gap Lock:对索引之间的“间隙”加锁(对第一条记录前的间隙和最后一条记录之后的间隙加锁),其他事务不能在间隙锁范围内进行插入数据。
- Next-key Lock:上面两者的结合。
除此之外还有页级锁,页级锁介于两者之间,是引擎BDB所采用的锁。
InnoDB的行级锁是建立在索引之上的,所以只有通过了索引的查询才会加锁,否则使用的是表锁。
如何安全地给小表加字段
在对数据库进行逻辑备份的时候会加上全局锁,对表的结构进行修改的时候,会对表加上表锁。
所以如果想要在线给一个表增加一个字段,那么可以选择在alter table中设定等待语句。
共享锁和排他锁
表级锁和行级锁可以进一步分为共享锁(s)和排他锁(x)。
- 共享锁:又称为读锁,一个事务对数据加上共享锁之后进行读取,这个期间内其他事务加共享锁而不能加排它锁。这保证了数据在事务读取的时候不会被更改。
- 排它锁:又称为写锁,一个事务对数据加上排他锁之后,其他事务不能对该数据增加任何锁直至排它锁释放。
除此之外,表级锁还有意向排它锁和意向共享锁,事务在对一个数据行加共享锁之前必须取得该表的意向共享锁。
乐观锁和悲观锁
悲观锁和乐观锁是一种概念,并非是数据库中实际存在的锁。
- 乐观锁:乐观锁不是数据库自带的,需要自己实现。在查询的时候,乐观地认为不会发生冲突,不进行额外的操作,在更新之后再去判断结果是否冲突。通常在查询的时候增加一个版本字段,每次操作的时候先进行查询版本号,然后在提交的时候判断版本号是否一致,如果一致则更新。
- 悲观锁:认为每次操作的时候都必须加锁。在MySQL中实际就是排他锁。
实际操作中数据库发生死锁了怎么办?
首先使用SHOW ENGINE INNODB STATUS
查看数据库日志,然后查找最近一次死锁发生的位置搜索关键字LATEST DETECTED DEADLOCK
,然后在后面的字段中就可以看出哪两个事务发生了死锁,死锁的时候在等待什么(日志来源)?
170219 13:31:31
*** (1) TRANSACTION:
TRANSACTION 2A8BD, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 448218, OS thread handle 0x2abe5fb5d700, query id 18923238 renjun.fangcloud.net 121.41.41.92 root updating
delete from test where a = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index a of table oauthdemo.test trx id 2A8BD lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
第一个部分是事务1,线程号为448218,执行到delete from test where a = 2
的时候卡住了,这时候正在申请a的X锁,所以提示lock_mode X waiting。
再看第二部分。
*** (2) TRANSACTION:
TRANSACTION 2A8BC, ACTIVE 18 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 2
MySQL thread id 448217, OS thread handle 0x2abe5fd65700, query id 18923239 renjun.fangcloud.net 121.41.41.92 root update
insert into test (id,a) values (10,2)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 923 n bits 80 index a of table oauthdemo.test trx id 2A8BC lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index a of table oauthdemo.test trx id 2A8BC lock mode S waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
线程48217在执行insert into test (id,a) values (10,2)
的事务的时候卡住了,这个时候正在申请S锁,这是因为a字段是一个唯一索引,所以insert语句会在插入前进行一次duplicate key的检查,为了使这次检查成功,需要申请S锁防止其他事务对a字段进行修改。
这个时候Mysql会自动让代价比较低的事务回滚,很明显事务2的操作代价更大,所以事务1被回滚了。
分布式事务
分布式事务常见的解决方案有如下几种:
二阶段提交
和MySQL中二阶段提交的方案一样,首先协调者先确认所有参与事务的数据库是否已经准备好了,只要有一个没有准备好就回滚该事务,如果都准备好了就提交事务。
我们一般会假设准备提交阶段之后的提交都是可靠的,如果真的有一个参与者提交失败或者超时就需要不停地重试直到这个参与者成功。
TCC
TCC的意思是try、confirm、cancel。这个方案的也非常简单,就是尝试进行事务,然后确认每个参与者的事务是否成功,如果不成功就取消该事务,对消耗的资源进行补偿。
异步确认/最终一致
这个方式是弱一致性的,这个方案就是指不同参与者之间通过消息进行事务,每个参与者定期扫描自己收到的消息,然后执行消息的任务并且回复。如果一个参与者一直没有能够执行任务,则消息的生产者会一直发送该消息直至成功。
这个方案由于是弱一致性的,所以适合用于并发量不高并且对一致性要求不高或者有其他方案作为逃生门的场景。
大表优化
当MySQL单表数据过大的时候,性能会明显下降,可以采用一些方法来进行优化。
单表优化
一般整型类型的表在千万级以下,字符串类型的表在五百万以下都是没有太大问题。只有当超过这个级别的时候才会需要进行单表优化。
单表优化的方式包括以下内容:
- 字段:尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,VARCHAR的长度只分配真正需要的空间等。
- 索引:索引针对性创建,考虑只在Where和order by命令上涉及的列建立索引。值分布很少的字段,比如性别不适合做索引等。
- 查询SQL优化:不做列运算,少用OR等。
读写分离
采用从库读主库写的方式来进行优化。
缓存
采用缓存可以加快数据查询的速度,一般缓存发生在以下层次:
- MySQL内部:可以通过参数开启,但是不是很实用。
- 数据访问层:比如MyBatis针对SQL语句做缓存,而Hibernate可以精确到单个记录。
- 应用服务层:这里可以通过编程手段对缓存做到更精确的控制和实现更多的策略。
- Web层:针对web页面做缓存。
- 浏览器客户端:用户端的缓存。
表分区
MySQL可以将表进行拆分,包括水平拆分和垂直拆分。
垂直拆分的优势在于使得列数据变小,在查询的时候减少读取的Block数,减少I/O次数等。
劣势在于主键会出现冗余,需要管理冗余列。
水平拆分的优势不存在单库大数据和高并发的性能瓶颈、提高系统稳定性和负载能力。
劣势在于分片事务一致性难以解决,跨节点Join性能差,逻辑复杂。
分片的原则在于尽量少拆分表、分片数量尽量少、数据分布尽量均匀等。
由于水平拆分的逻辑比较复杂,所以有了不少成熟的解决方案,这些方案分为两部分:客户端架构和代理架构。
- 客户端架构:通过修改数据访问层,比如JDBC、Data Source、MyBatis,通过配置来管理多个数据源,并在模块诶完成数据的分片整合,一般以Jar包的方式实现。
- 代理架构:通过独立的中间件来统一管理所有的数据源和数据分配整合。
Order by优化
order by是常用的排序语句,有时候我们需要先进行查询,然后对查询结果进行排序,最后取前面几行(TopK)问题。
例如,select city,name,age from table where city='杭州' order by name limit 1000
。
那么这条语句是如何执行的呢?
首先他会先根据建立的索引进行查询,比如我们在city字段上建立了索引,那么首先会根据索引进行查询,查询到主键id之后就先回表,把剩余的信息(name,age)都取出来放入到sort_buffer中,然后再取下一个id,再把信息放入到buffer中直到city的值不满足条件查询。
然后在buffer中对name字段进行快速排序(如果太大的话需要进行外部排序),最后返回排序结果的前1000行。
先把所有字段读入内存再进行排序的操作我们成为全字段排序。
但是当查询的字段太多,以至于超过了mysql设置的最大行长度的时候,sort_buffer中就只会保存需要排序的name字段以及主键id。
当排序完成之后,因为Buffer中只有name和id所以还需要再进行一次回表查询,速度就慢了下来。
在buffer中只有name和id字段的排序我们称为rowid排序。
从结果上来说全字段排序的速度肯定是比rowid排序的速度快上不少的。
那么能不能不进行排序呢?
上面这个操作之所以生成临时表的原因是需要对name字段进行排序,那么如果name字段在读取的时候已经有序了,是不是就不用排序了呢?
为了使得name有序,我们就可以使用联合索引(city,name)。使用联合索引之后,name就已经是有序的了,直接在结果集中返回就行。
那么对于select * from t where city in("杭州","苏州") order by name limit 100
呢?
显然in("杭州","苏州")
是一个范围查询,那么联合索引走到这里就中断了。
那么对于范围不太大的查询,我们可以在java层面实现分次查询,首先对select * from t where city = "杭州" order by name limit 100
和select * from t where city = "苏州" order by name limit 100
进行两次查询,然后对两个结果集进行归并排序,最后再取出前100。
但是如果是select * from t where city in("杭州","苏州") order by name limit 10000,10
呢?
这样返回给客户端的数据量就非常大,我们可以采用rowid排序的思路,同样的首先查询select id,name from t where city = "杭州" order by name limit 100010
和select id,name from t where city = "苏州" order by name limit 100010
,然后进行归并,最后再根据归并的前100个id到数据库中读取。
Limit优化
首先先要明白limit是如何操作的,limit [offset] row
在这个标准的limit语句中,offset指的是偏移量,row指的是偏移量后面的行数。对于limit 100000,20
来说就先读出前面的100020行,然后丢弃前面的100000行。
所以对于大表来说,直接使用limit语句执行效率会非常低下。
那么如何进行优化呢?
先考虑下我们使用limit的场景,在我们项目中使用比较多的地方是前端分页,那么如果是根据主键查询,完全可以先计算出这一页的偏移,然后直接使用where
语句来查询。
比如一页20行数据,那么第十页就是190~200的偏移。
where id >= 190 and id <= 200
但是有些时候偏移量的计算不是那么显著的。
那么我们可以先借助索引覆盖来进行优化。
select * from table where ID>=(select id from table limit 100000,1) limit 20
。
但是有时候查询比较复杂,比如使用了where进行范围查询,那么这个时候就不能直接计算主键的偏移了。
可以考虑存放到临时表中,每次访问临时表的记录。
Join优化
对于一条join语句select * from t1 join t2 on t1.a = t2.a
。
在这个join语句中没有指定哪个表强制作为驱动表,所以mysql会自己进行优化,选择一张比较小的表作为驱动表。
我们假设t1是驱动表,t2是被驱动表,那么执行的过程就是先查询出t1表中的一行数据R,然后根据这行数据中的a字段在t2表中进行查询,取出t2表中满足条件的行,然后和t1表中的行组成一行。
在这个过程中,查询t2的时候是可以使用a上的索引的。
这种join方式被称为"index Nested-Loop Join",简称NLJ,它的时间复杂度为N+N2log2M,N为t1表的数量,M为t2表的数量。
显然N对时间复杂度的影响更大,所以选择一张小表作为驱动表的速度会快上不少,这里的小表指的是结果集比较小的表,而不是整张表的大小。
NLJ的前提是被驱动表可以用上索引,如果用不上索引,就只能采用Block Nested-Loop join了.
直接进行匹配的话时间复杂度为N*M,效率非常低,所以MySQL就提出了优化Block Nested-Loop join。
这个算法的意思就是首先将t1表全部读到内存里面,然后把t2中的每一行取出来,逐行匹配,如果有匹配上的就加入结果集,没有就丢弃。这种做法虽然结果上还是N*M的复杂度,但是只需要进行N+M次的全表查询就可以了,其余的判断都在内存中完成。
但是如果t1表太大,内存中放不下呢?
那就采用分段放的策略,每次取出一部分t1表的数据,匹配完这块数据之后清空buffer,再取出下一块数据。
总的来说,如果要使用join语句的话最好被驱动表的匹配字段上有索引,这样的查询速度会快很多。
Group by优化
select id%10 as m, count(*) as c from t1 group by m
group by的执行过程是这样的,首先建立一个临时表,表中有两个字段m和c,主键是m,然后扫描t1表的索引,如果临时表中没有主键为x的行就行插入,如果有的话就讲x这行的c值加1。最后再根据m进行排序,返回结果集。
如果业务需求中不需要对m进行排序,可以选择加上order by null
。
由于group by需要使用临时表保存结果,所以当内存不够的时候会转变为硬盘临时表,开销非常大。所以根据需要可以选择增加内存临时表的临界值或者使用SQL_BIG_RESULT
来告诉优化器直接使用磁盘临时表,MySQL会自动生成一个排好序的数组,对这个数据进行group by速度就快多了。
数据库连接池
在没有数据库连接池的情况下,我们访问数据库的过程是:
- 加载数据库驱动程序。
- 通过JDBC建立数据库连接。
- 访问数据库,执行sql语句。
- 断开数据库连接。
但是建立数据库连接是一个非常费时的操作,如果频繁地进行数据库连接操作将占用大量的系统资源。并且每一次数据库连接使用完之后必须关闭,如果关闭异常还会出现内存泄漏。
为了解决上述问题,最后采用了资源池设计模式。
先建立一个数据库的连接池,当请求数据库连接的时候首先查看是否有空闲连接,如果存在则分配给请求,并将引用数+1,如果没有则查看是否到了最大连接数,没有到达最大连接数的话则先创建一个新的数据库连接加入连接池中,如果达到就按设定的最大等待时间进行等待。
释放连接的时候,首先看该连接的引用次数是否超过了规定值,如果超过了就关闭该连接,如果关闭之后超过了最小连接数,则将连接池填充满。
范式
数据库的范式一共分为三级:
- 第一范式:字段不可再分,原子性。
- 第二范式:满足第一范式的条件下,一个表只能说明一个事物,非主键必须完全依赖于主键属性。
- 第三范式:在满足第二范式的条件下,每列都有与主键的直接关系,不存在传递依赖。
一条SQL语句是如何在MySQL中执行的
MySQL基本架构
MySQL主要分为Sever层和存储引擎。
存储引擎上面已经介绍过了就不再赘述,这边主要介绍Sever层,Sever层主要包括:
- 连接器:身份认证和权限相关。
- 查询缓存:执行查询语句的时候会先查询缓存,缓存中存放了一个key-value,key是查询语句,value是结果集。
- 分析器:没有命中缓存的话就会通过分析器,进行词法分析和语法分析,提取出关键字和判断语法是否正确。
- 优化器:MySQL通过自己的判断选择一个最优执行方案去执行。
- 执行器:选择了执行方案之后,准备开始执行,先判断该用户是否有权限,如果没有的话则返回错误信息。
查询语句
查询语句首先判断该语句是否有权限,如果没有权限则直接返回错误信息,如果有权限的话则先查看缓存(如果启动了缓存的话)
然后通过分析器进行词法分析,判断语句是否出错。
然后由优化器选择执行方案,最后判断进行权限校验,如果通过则直接返回执行结果。
更新语句
更新语句大致和查询语句相同,但是多出了记录日志的部分。
数据库日志
其实Mysql中最重要的就是日志系统,我之前居然直接跳过了,这次还是补充上。
redo日志
Mysql更新数据并非是每次都直接到磁盘上,这样每次都要IO,效率太低。
在InnoDB引擎中首先会把更新记录到一个叫做redo log的日志中,然后等到数据库空闲的时候或者redo日志即将写满的时候就会把redo日志中的信息刷新到磁盘上。
所以有时候,sql变慢也有可能是正在把redo日志的信息刷新到磁盘上。
redo日志的大小是固定的,一个有4个redo log,每个1G,采用循环队列的方式,如果后面的日志写满了就会从第一个日志开始覆盖。所以和一般的循环队列类似,redo日志也存在两个指针,一个是当前写位置叫做write pos,一个是当前日志的尾部叫做check piont。
当write pos追上check piont的时候,InnoDB就需要把中间的一部分数据刷新到磁盘上,这个能力叫做crash-safe。
有了redo log,即使数据库发生异常重启,之前的操作记录也不会丢失。
bin日志
bin log和redo log有些相似,但是是运行在Server层上的。之前的MyISAM引擎是没有redo日志的,所以依靠bin log来完成crash-safe。
这两个日志的区别如下:
- redo log是InnoDB引擎特有的,但是bin log是在Server层上的,是所有引擎共有的。
- redo log是物理日志,记录的是“在某个数据页上做了什么修改”,bin log是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2的这一行的c字段增加1”(根据具体格式不同,可以是sql语句也可以是更新前后的状态)。
- redo log是循环写的,空间固定会用完,但是bin log是追加写的,达到一定大小之后会换下个文件,不会覆盖。
二阶段提交
既然存在两个记录日志,那么保证两个日志的一致性也是一个非常重要的事情。
在InnoDB中采用了一个叫做二阶段提交的功能。
对于一条update语句来说,执行步骤如下:
- 数据页是否在内存中,如果不在则需要把数据页读入内存。
- 在内存更新对应行
- 将更新操作写入redo log,进入prepare阶段
- 写bin log
- 提交事务,处于commit状态。
那么如果不采用二阶段提交会发生什么事情呢?
- 假设先写redo日志,再写bin日志,那么如果redo日志写完之后,bin日志写到一半的时候,突然断电了,那么数据库重启之后仍然会根据redo的记录刷新到磁盘上,但是当数据需要回滚的时候,数据库是根据bin log进行回滚的,就会导致这行数据少了一次更新。
- 先写bin log的话,redo日志没有完成就会回滚事务,但是bin log日志中已经有了记录,那么回滚数据库的时候就会导致多了一次更新。、
所以总的来说,如果不采用二阶段提交就会导致数据库的状态和回滚的状态不一致。
undo日志
undo log是用于MVCC(多版本并发控制)的日志,事务的读取已提交和可重复读就依靠undo log来实现。
undo log中记录的是回滚操作,也就是如果要将当前版本的值回滚到上个版本需要进行什么操作。
以可重复读为例,当一个事务开始的时候会记录下当前数据的版本号,当需要进行第二次读操作的时候,如果这个数据的版本号已经和开始时候的版本不一致了,那么就会执行回滚操作,回滚到事务开始时候的版本号。
Count
count(*)的实现方式
首先来说以下count(*)
是怎么实现的。
在MyISAM中把一个表的总行数存在了磁盘上,所以执行count(*)
的时候会直接返回这个数值,所以查询速度非常快,但是如果需要使用where语句,那么还是需要全表查询的。
如果是InnoDB的话,由于需要进行MVCC,查询出来的行数也会有多个版本号,所以无法像MyISAM那样可以把行数存放在磁盘上,只能进行一次全表查询。
但是其实MySQL也是做了优化的,因为在InnoDB中普通索引存放的是主键的地址,所以普通索引会比主键索引树小不少,那么在查询的是InnoDB就会选择比较小的普通索引而非主键索引。
MySQL中的show table status命令也可以查询出当前表有多少行,但是这个数据是不准确的。之前说到数据库选择索引的时候是使用采样的方式的,所以有时候会不太准确,这个命令也是这样,官方文档说误差可以达到40%~50%。
那么如何快速的查询出count呢?可以采用的方法之一就是增加一张表专门用于保存其他表的行数,这样在事务中更新的时候顺便把这张表也更新一下,可以使得速度快上不少的同时不会对MVCC造成影响。
不同count的做法
首先我们需要明白count究竟是一个什么操作?
count()本身是一个聚合函数,会逐行判断里面的参数是否是NULL,但是根据主键还是普通字段会有所不同的操作。
由于主键一般是not null的,所以InnoDB会进行全表查询,然后把id返回给Server层,Server层拿到id后判断是不可能为空的,就按行累加。
但是对于普通字段来说,不一定是not null的,所以对于可以为null的行还需要把值取出来一下,不是null才累加。
count(1)
则是对每一行插入一个1,查看是否能加入1,能加入就累加。
而使用最多的count(*)
进行过优化,不需要把值取出来,直接按行进行累加,所以速度会比其他的方法快一些。
总的来说,count(字段)<count(id)<count(1)=count(*)。
删除表和字段
在MySQL中删除表和字段的操作和OS比较相似,都是直接把这个空间标记为可复用。
但是这样的操作和OS也会有同样的问题,即会造成空间的碎片化,有时候会出现明明有800行的空间却无法插入的问题(因为这800行是离散的,而存储要求连续的)。
所以如果想要使得删除记录之后收缩空间,就需要新建一个表,然后把原来的数据复制到新的表上去,有点类似于GC的标记复制算法,所以他也和GC有着同样的问题——不能并行,必须暂停表的操作,即先锁住表。
当然就像GC出现了并行垃圾回收器一样,MySQL在5.6之后也采用了一个叫做Online DDL操作。简单来说就是并行的把数据复制到临时表上,然后在复制的同时记录下复制时候对表进行的操作,等到复制完了再回放记录下的操作,最后使用临时表替换原来的表。
可以看到MySQL和OS、GC有着相似的原理。
Explain
explain用于查看数据库中sql语句执行的情况,其中有多个字段,下面就逐一解释这些字段的作用和意义。
sql语句和执行结果如下
EXPLAIN SELECT * FROM tblnomio WHERE tblnomio.niogctid = 'A0232'
- id:id指的是select的查询序号,表示查询中执行select子句或者操作表的顺序。id越大执行优先级越高,相同id则从上到下执行。
- select_type:指查询的类型,如果是简单的查询则是Simple,如果有包含其他复杂的子部分,最外层是Primary,子查询是Subsquery
- table:相关的表
- type:访问类型,上图中的ref表示非唯一普通索引,eq_ref表示普通索引,range表示范围,index表示遍历全部索引,all表示全表查询。
- possible_keys:可能使用的索引
- key:实际使用的索引
- key_len:索引的字节数
- ref:索引的哪一列被使用了,在上面的查询中是查询一个常数值,所以是const,如果是
where id = niogctid
那么就是niogctid
- row:查询的行数
- extra:一些额外信息,比如using index,表示索引覆盖,using where表示使用过滤,同样需要使用
where id = niogctid
,using temporary,表示使用了临时表保存中间结果,常见于order by和分组查询group by。
自己动手实现一个数据库连接池
这道也是常见的面试题,我想对于一个应届生来说,如果能写出来的话应该能加分不少。
不过话说回来,感觉写一个连接池比写一个线程池简单一些。总结一下要点:
- 建立一个连接队列用于管理连接。
- 通过
DriverManager.getConnection(url,user,password)
方法来新增一个数据库连接,然后代理其close()
方法。 - 在
close()
方法中进行判断,如果连接池的连接数量小于核心数数量则加入队列,否则直接释放。 - 在
getConnection()
方法中进行判断,如果当前队列为空,说明已经达到核心数,判断一下是否还能增加连接,如果可以则新建连接,否则抛出异常。 - 在代理的时候要注意三个参数是类加载器、类的接口和一个
invocationHandler
接口。
public class MyDataConn {
private Deque<Connection> connectionQueue = new LinkedList<>();
private final int init = 10;
private final int max = 20;
//保证可见性
private volatile int size = 10;
//初始化
public MyDataConn(){
for(int i=0;i<10;i++){
try {
connectionQueue.add(createConnection());
}catch (Exception ex){
}
}
}
//从队列中返回一个连接,或者直接新建一个连接
public Connection getConnection(){
if(connectionQueue.size() > 0)
return connectionQueue.pollFirst();
if(size < max){
size++;
return createConnection();
}
throw new RuntimeException("超过最大连接数");
}
//新建一个连接,并且返回其代理
public Connection createConnection(){
try{
final Connection conn = DriverManager.getConnection("jdbc:mysql:///jdbc_demo","root","123456789");
Connection connProxy = (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(), new Class[]{Connection.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("close")){
releaseConnection(conn);
return null;
}else{
return method.invoke(conn,args);
}
}
});
return connProxy;
}catch (Exception ex){
throw new RuntimeException(ex);
}
}
//释放连接
private void releaseConnection(Connection conn) {
if(connectionQueue.size()<init){
connectionQueue.addLast(conn);
}else{
try {
size--;
conn.close();
}catch (Exception ex){
throw new RuntimeException(ex);
}
}
System.out.println("当前数量"+connectionQueue.size());
}
}
后记
这篇文章主要对MySQL的相关知识做一个总结,主要包括了索引、事务、引擎等内容。
主要参考内容如下:
MySQL的字符集和校对规则
深入理解 Mysql 索引底层原理
mysql覆盖索引与回表
无语,我差点被面试官怼坏了,又给我问到MySQL索引
【Java面试题】数据库连接池Java代码实现
《MySQL45讲》
以及内推军和JavaGuide的参考资料。
7.27更新数据库连接池的简单实现。