MySQL知识体系的三驾马车
在我看来要掌握好MySQL的话要理解好这三个东西:
- 索引(B+树)
- 日志(WAL)
- 事务(可见性)
索引决定了查询的性能,也是用户感知到的数据库的关键所在,日常使用过程中抱怨最多的就是查询太慢了;
而日志是一个数据库的灵魂,他决定了数据库为什么可靠,还要保证性能,核心原理就是将随机写转换成顺序写;
事务则是数据库的皇冠。
索引
索引主要是解决查询性能的问题,数据一般都是写少查多,而且要满足各种查,所以使用数据库过程中最常见的问题就是索引的优化。
MySQL选择B+树来当索引的数据结构,是因为B+树的树干只有索引,能使得索引保持比较小,更容易加载到内存中;数据全部放在B+树的叶节点上,整个叶节点又是个有序双向链表,这样非常合适区间查找。
如果用平衡二叉树当索引,想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的
对比一下 InnoDB 的一个整数字段B+数索引为例,B+树的杈数一般是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。
明确以下几点:
- B+树是N叉树,以一个整数字段索引来看,N基本等于1200。数据库里的树高一般在2-4层。
- 索引的树根节点一定在内存中,第二层大概率也在内存,再下层基本都是在磁盘中。
- 每往下读一层就要进行一次磁盘IO。 从B+树的检索过程如下图所示:
每往下读一层就会进行一次磁盘IO,然后会一次性读取一些连续的数据放入内存中。
一个22.1G容量的表, 只需要高度为3的B+树就能存储,如果拓展到4层,可以存放25T的容量。但主要占内存的部分是叶子节点中的整行数据,非叶子节点全部加载到内存只需要18.8M。
联合索引
对于多个查询条件的复杂查询要正确建立多列的联合索引来尽可能多地命中多个查询条件,过滤性好的列要放在联合索引的前面。
MySQL一个查询只能用一个索引。
索引下推(index condition pushdown )
对于多个where条件的话,如果索引只能命中一个,剩下的那个条件过滤还是会通过回表来获取到后判断是否符合,但是MySQL5.6后,如果剩下的那个条件在联合索引上(但是因为第一个条件是模糊查询,没法用全联合索引),会将这个条件下推到索引判断上,来减少回表次数。这叫索引下推优化(index condition pushdown )
覆盖索引
要查询的列(select后面的列)如果都在索引上,那么这个查询的最终结果都可以直接从索引上读取到,这样读一次索引(数据小、顺序读)性能非常好。否则的话需要回表去获取别的列
前缀索引用不上覆盖索引对查询性能的优化,每次索引命中可能需要做一次回表,确认完整列值
回表
select from table order by id limit 150000,10 这样limit后偏移很大一个值的查询,会因为回表*导致非常慢。
这是因为根据id列上索引去查询过滤,但是select *要求查所有列的内容,但是索引上只有id的数据,所以导致每次对id索引进行过滤都要求去回表(根据id到表空间取到这个id行所有列的值),每一行都要回表导致这里出现了150000+10次随机磁盘读。
可以通过先用一个子查询(select id from order by id limit 150000,10),子查询中只查id列,而id的值都在索引上,用上了覆盖索引来避免回表。
先查到这10个id(扫描行数还是150000+10, 这里的limit因为有deleted记录、每行大小不一样等因素影响,没法一次跳到150000处。但是这次扫描150000行的时候不需要回表,所以速度快多了),然后再跟整个表做jion(join的时候只需要对这10个id行进行回表),来提升性能。
索引的一些其它知识点
多用自增主键是因为自增主键保证的是主键一直是增加的,也就是不会在索引中间插入,这样的话避免的索引页的分裂(代价很高)
写数据除了记录redo-log之外在内存中还会在内存(change buffer)中记录下修改后的数据,这样再次修改、读取的话不需要从磁盘读取数据,非唯一索引才能用上change buffer,因为唯一索引一定需要读磁盘验证唯一性,既然读过磁盘这个change buffer的意义就不大了。
|
<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 10px; color: rgb(134, 145, 148); background: rgb(239, 242, 243); line-height: 1.6; border: none; text-align: right;">
1
</pre>
|
<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 575px;">
mysql> insert into t(id,k) values(id1,k1),(id2,k2);//假设k1页在buffer中,k2不在
</pre>
|
日志
数据库的关键瓶颈在于写,因为每次更新都要落盘防止丢数据,而磁盘最怕的就是随机写。
Write-Ahead logging(WAL)
写磁盘前先写日志,这样不用担心丢数据问题,写日志又是一个顺序写,性能比随机写好多了,这样将性能很差的随机写转换成了顺序写。然后每过一段时间将这些日志合并后真正写入到表空间,这次是随机写,但是有机会将多个写合并成一个,比如多个写在同一个Page上。
这是数据库优化的关键。
bin-log
MySQL Server用来记录执行修改数据的SQL,Replication基本就是复制并重放这个日志。有statement、row和混合模式三种
redo-log
INNODB引擎用来保证事务的完整性,也就是crash-safe。MySQL 默认是保证不了不丢数据的,如果写了表空间还没来得及写bin-log就会造成主从数据不一致;或者在事务中需要执行多个SQL,bin-log保证不了完整性。
而在redo-log中任何修改都会先记录到redo-log中,即使断电MySQL重启后也会先检查redo-log将redo-log中记录了但是没有提交到表空间的数据进行提交(刷脏)
redo-log和bin-log的比较:
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。redo-log保证了crash-safe的问题,binlog只能用于归档,保证不了safe。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
redo-log中记录的是对页的操作,而不是修改后的数据页,buffer pool(或者说change buffer)中记录的才是数据页。正常刷脏是指的将change buffer中的脏页刷到磁盘,如果没来得及刷脏就崩溃了,那么就只能从redo-log来将没有刷盘的操作再执行一次让他们真正落盘
只有当commit(非两阶段的commit)的时候才会真正把redo-log写到磁盘上。
binlog和redo-log一致性的保证
bin-log和redo-log的一致性是通过两阶段提交来保证的,两阶段提交过程中prepare是非常重的,prepare一定会持久化(日志),记录如何commit和rollback,一旦prepare成功就一定能commit和rollback,如果其他节点commit后崩溃,恢复后会有一个协商过程,其它节点发现崩溃节点已经commit,所以会跟随commit;如果崩溃节点还没有prepare那么其它节点只能rollback。
实际崩溃后恢复时MySQL是这样保证redo-log和bin-log的完整性的:
- 如果redo-log里面的事务是完整的,也就是有了commit标识,那么直接提交
- 如果redo-log里面事务只有完整的prepare,则去检查事务对应的binlog是否完整
- 如果binlog完整则提交事务
- 如果不完整则回滚事务
- redo-log和binlog有一个共同的数据字段叫XID将他们关联起来
刷脏
在内存中修改了,已经写入到redo-log中,但是还没来得及写入表空间的数据叫做脏页,MySQL过一段时间就需要刷脏,刷脏最容易造成MySQL的卡顿。
- redo-log写满后,系统会停止所有更新操作,把checkpoint向前推进也就是将数据写入到表空间。
- 系统内存不够,也需要将内存中的脏页释放,释放前需要先刷入到表空间。
- 系统空闲的时候也会趁机刷脏
在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开
事务
可见性
是基于事务的隔离级别而言的,常用的事务的隔离级别有可重复读RR(Repeatable Read,MySQL默认的事务隔离级别)和读已提交RC(Read Committed)。
可重复读
读已提交:A事务能读到B事务已经commit了的结果,即使B事务开始时间晚于A事务
重复读的定义:一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
指的是在一个事务中先后两次读到的结果是一样的,当然这两次读的中间自己没有修改这个数据,如果自己修改了就是当前读了。
如果两次读过程中,有一个别的事务修改了数据并提交了,第二次读到的还是别的事务修改前的数据,也就是这个修改后的数据不可见,因为别的事务在本事务之后。
如果一个在本事务启动之后的事务已经提交了,本事务会读到最新的数据,但是因为隔离级别的设置,会要求MySQL判断这个数据不可见,这样只能按照undo-log去反推修改前的数据,如果有很多这样的已经提交的事务,那么需要反推很多次,也会造成卡顿。
总结下,可见性的关键在于两个事务开始的先后关系:
- 如果是可重复读RR(Repeatable Read),后开始的事务提交的结果对前面的事务不可见
- 如果是读已提交RC(Read Committed),后开始的事务提交的结果对前面的事务可见
当前读
更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。除了 update 语句外,select 语句如果加锁,也是当前读。
总结
理解好索引是程序员是否掌握数据库的最关键知识点,理解好索引才会写出更高效的SQL,避免慢查询搞死MySQL。
对日志的理解可以看到一个数据库为了提升性能(刷磁盘的瓶颈)采取的各种手段。也是最重要的一些设计思想所在。
事务则是数据库皇冠。