详解MySQL
三大日志
重做日志(redo-log)
- (用来干嘛的) InnoDB 存储引擎独有,MySQL崩溃时用来恢复数据:重新执行 Redo Log 中尚未持久化到数据文件的修改操作;
- (前置知识)MySQL的查询会把数据页读到缓冲池Buffer Pool中,更新数据会直接在Buffer Pool操作;
- (如何做的)把“在某个数据页上做了什么操作”记录到重做日志缓存redo log buffer中,再刷盘;
- (事务的特殊)事务执行过程的 redo log 会写入 redo log buffer 中,因为可能会被后台线程刷盘,所以没有提交事务的 redo log 记录也可能被写入磁盘;
- (存储形式)硬盘上存储的 redo log 是以一个日志文件组的形式出现的,每个的 redo 日志文件大小都是一样的,会进行循环写入;
二进制日志(bin-log)
- (做啥的)逻辑日志,顺序记录所有涉及更新数据的逻辑操作;主从|主备依靠它同步;
- (记录格式)statement记录SQL原文;row包含操作的数据,例如
now()
函数的具体时间,是解析后的SQL语句;mixed会判断是否引起不一致,再选择; - (写入机制)先写到binlog cache,事务提交时再一起写到binlog中,每个事务(线程)的binlog cache是独立的;
两阶段提交
(问题所在)binlog和redo log的逻辑可能不一样,即事务中写了redo log,但binlog最终没写入;
(解决方式)把redo log的写入拆分为prepare和commit两个步骤,在提交事务时先写入binlog,再设置redo log的commit;在重启时需要满足既不是commit阶段又不存在对应的binlog,才回滚事务;
图源:JavaGuide
回滚日志(undo-log)
- 回滚日志,用来对事务进行回滚,保证事务的原子性;
- 也需要存储到redo log中,但是事务提交后是可以删除的;删除的时机和undo log的种类有关,和MVCC有关,详见MVCC
事务的ACID:
- 原子性(Atomic):修改数据要么全部成功,要么全部失败
- 一致性(Consistent):数据都保持一致状态?
- 隔离性(Isolation):并发事务之间的隔离
- 持久性(Duration):事务完成之后不会被回滚
事务隔离级别
4种级别
- READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
RC
和RR
两种级别都是用MVCC机制,但是他们生成Read View
的时机不同,RC会在每次select查询前都生成一个Read View,而RR只在事务开始后第一次select数据前生成Read View,这样RR就实现了可重复读
导致的问题
- 脏读(读未提交) => 读已提交
图源:JavaGuide
- 不可重复读(一个事务中读一个数据多次,结果不一样) => 可重复读
图源:JavaGuide
- 幻读 => 可重复读
例子:SQL 脚本 1 在第一次查询工资为 500 的记录时只有一条,SQL 脚本 2 插入了一条工资为 500 的记录,提交之后;SQL 脚本 1 在同一个事务中再次使用当前读查询发现出现了两条工资为 500 的记录这种就是幻读。
图源:JavaGuide
注:图中的
select ... for update
的for update
会将select得到的行(如果没有主键,则全表上锁)上悲观锁,用于select之后update。
解决方案:一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据。
- 将事务隔离级别调整为
SERIALIZABLE
; - 在可重复读的事务级别下,给事务操作的这张表添加表锁;
- 在可重复读的事务级别下,给事务操作的这张表添加
Next-key Lock(Record Lock+Gap Lock)
;⚠️待研究
多版本并发控制(MVCC)
MVCC即多版本并发控制,通过维护在每个数据行上的多个版本来实现;这些版本会定期被回收
总述(基于什么逻辑实现的):
- 修改:即写操作,会生成一个新的数据版本,带有当前事务的版本号,从而使得其他事物能够读取相应版本的数据
- 读取:使用快照读取,选择符合其事务开始时间的数据版本进行读取
- 事务提交后,其版本会成为数据库的最新版本,并对其他事务可见
快照读和当前读
mysql的repeatable-read
隔离级别其实一定程度上可以解决幻读问题,这就是用MVCC保证的,关键在于快照读和当前读这两个操作。
快照读(一致性非锁定读):读取历史快照的数据;
锁定读(当前读):执行
select ... lock in share mode
、select ... for update
、insert, update, delete
这些语句的时候,就不能再用快照了,而是会读取最新版本并且对读取到的记录加锁。
共享锁和排它锁
select ... lock in share mode
会对数据行添加**共享锁(S锁),有S锁再添加排他锁(X锁)**会阻塞,因为有人在读不准改;- 如果是
insert, update, delete
,会添加X锁,此时其他任何锁都不能加,因为我在改
但是即使上S锁或X锁,快照读还是能读的,因为读的是第一次查询时生成的快照
InnoDB对MVCC的实现
前面的MVCC只是一种理论,具体的实现方式要看不同的引擎,比如说快照读,不可能真的存一个快照在本地,太浪费空间了。
当在一个事务中读取某一行时,会根据这个事务的Read View及和该行的隐藏字段DB_TRX_ID
(最后一次插入或更新该行的事务id)来判断,该行的最新版本是否对事务可见;如果不可见,则通过行的另一个隐藏字段DB_ROLL_PTR
回滚到上一个版本,直到某个版本对该事务可见,或回滚指针不存在,即该版本首次被insert。
数据行隐藏字段
(重要)
DB_TRX_ID
:表示最后一次插入或更新该行的事务 id(重要)
DB_ROLL_PTR
:回滚指针,指向该行的undo log
DB_ROW_ID
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引
ReadView结合undo-log
Read View创建时会有:
- 当前活跃事务列表
m_ids
:不一定是连续的,因为后创建的可能先完成后,便不活跃了 m_low_limit_id
:目前出现过的最大的事务 ID+1m_up_limit_id
:活跃事务列表m_ids中最小的事务 ID,ID比它大的并不一定活跃,也可能完成了,但比他小的是必然完成了
undo-log有两个作用:
- 事务回滚时恢复数据
- MVCC当读取某个行记录,发现该记录的最新版本对当前事务并不可见,则需要通过undo log读取之前的版本数据,从而实现非锁定读
有两种undo-log:
insert undo log
:因为insert
操作的记录只对事务本身可见,对其他事务不可见,故该undo log
可以在事务提交后直接删除,所以如果某数据行的DB_TRX_ID
表明该数据行不可见,它的DB_ROLL_PTR
又是空的话,这就说明他是insert的产生的update undo log
:update
或delete
操作中产生的undo log
。该undo log
可能需要提供MVCC
机制,因此不能在事务提交时就进行删除。提交时放入undo log
链表,等待purge线程
进行最后的删除。不同事务或者相同事务的对同一记录行的修改,会使该记录行的
undo log
成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。
数据表索引
数据结构
哈希表
(怎么建和怎么发挥作用)key为建立索引的字段值,value为该行所在的地址(或index);
(哈希冲突)拉链法和红黑树or B+Tree
(缺点)不支持顺序和范围查询,无序;例如找id小于500,需要对1和499都定位一次
红黑树
待研究
B树
多叉平衡搜索树,参考https://www.bilibili.com/video/BV1tJ4m1w7yR,下图亦是出自于此
比红黑树好——因为大量数据需要对磁盘进行IO,内存放不下
而B+树每个节点存储的数据多,硬盘读取1次节点耗时是类似的,无论节点的元素有多少
访问节点的操作在硬盘上进行,访问节点内的元素是在内存上进行的
- 整体概述:m阶B树的结点,最多m个分支和m-1个元素,最少m/2向上取整个分支,和m/2向上取整个-1个元素
- 插入,只可能上溢出,溢出后需要分裂和调整
- 删除,只可能下溢出;删除非叶节点的情况,去删除其前驱或后继,转换为删除叶节点
B+树
分支数目=节点的元素数,每个元素都会出现在叶节点,中间节点的元素是重复的
叶节点存储数据所在地址,可以针对一张表中多个字段建立B+索引
是一个多极索引结构,叶节点可理解为表格主键的索引,树中的第i层是对i+1层的索引
索引介绍
主键索引
每个表都会有主键并建立索引,会检查是否有唯一索引且不允许null的字段,拿来当作默认主键,否则会自动创建一个自增主键;
InnoDB
默认使用B+树存储主键索引,这时候叶节点就会存储指向相应记录的指针;InnoDB
使用聚簇索引,MyISAM
使用非聚簇索引。前者使得基于主键的查询效率非常高,因为表中数据的物理顺序和主键索引的顺序是一致的,但是更新的代价比较大;后者比前者多一步IO操作,并且可能会导致二次查询,但是更新代价小;二级索引
对应于主键索引,通过二级索引可以定位主键的位置;如果也用B+树,叶节点存储的是索引列的值和对应的主键值,二级索引需要找两次索引树
覆盖索引:索引包含所需要查询字段的所有值。一般而言,非主键索引查询的时候需要先找到主键值,然后再定位完整的行数据,这叫回表;但是如果需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,而无需回表查询。即少一次查询索引树
联合索引:使用表中的多个字段创建索引,其实主要还得看最左的字段,即最左匹配原则。将区分度高的字段放在最左边,可以过滤更多数据。
假设有一个联合索引
(column1, column2, column3)
,其从左到右的所有前缀为(column1)
、(column1, column2)
、(column1, column2, column3)
(创建 1 个联合索引相当于创建了 3 个索引),包含这些列的所有查询都会走索引而不会全表扫描。只有联合索引的前缀会生效。[!NOTE]
什么时候不走索引呢,即索引失效问题,对于联合索引,只要不包含最左列,就不会走索引,其余情况待深入研究
- 前缀索引:只针对字符串类型,对前几个字符创建索引,建立起的索引更小。
- 聚簇索引和非聚簇索引:聚簇索引中数据行和索引值是存放在一起的,例如经典例子B+树存主键索引,它会比非聚簇索引少一步根据叶节点的指针去找对应数据行的操作,因为聚簇索引直接把数据行存在叶子结点中,并且叶子结点是可以顺序访问的,每个叶子结点都有指向下一个叶子结点的指针。
执行计划分析
Explain关键字+查询语句
比较重要的字段
type
下面是type字段可能的值,效率依次变差
- system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。
- const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。
- eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式,常用于使用主键或唯一索引的所有字段作为连表条件。
- ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。
- index_merge:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。
- range:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。
- index:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。
- ALL:全表扫描。
possible_keys & key
possible_keys是可能用到的索引,key是实际用到的索引。InnoDB会选择更优秀的索引来查找数据。通过对比这两个字段,可以分析查询引擎的推断。
Extra
extra包含额外的查询信息,例如:
Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。
Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY。
Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。
Using index condition:表示查询优化器选择使用了索引条件下推这个特性。
Using where:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。
**Using join buffer (Block Nested Loop)**:连表查询的方式,表示当被驱动表的没有使用索引的时候,MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。