MySQL的存储,索引,锁以及事务
# MySQL的存储,索引,锁以及事务
本文是我对MySQL中几个重点部分的理解与概要,阅读对象是我自己。
对最近MySQL中几个重要内容的概要总结,方便回顾与复习。
# 0. 重要的参考与学习资料
首先是两本书:
- 《MySQL必知必会》
- 《MySQL技术内幕 — InnoDB存储引擎》
接下来是一些博客网站
首先是刚刚发现的宝藏博主的文章,文章非常精湛,配图也很棒。
- 『浅入浅出』MySQL 和 InnoDB (opens new window),这个是捋一下大体思路,框架性的内容,十分重要,可以快速对InnoDB的重点进行复习。
- 为什么MySQL使用B+树 (opens new window),然后讨论了B+树的一些优势所在,这是一个常被问到的点。
- 『浅入深出』MySQL 中事务的实现 (opens new window),最后是关于事务的实现部分,这部分在《技术内幕》那本书里讲的太详细了,不适合快速学习,可以用作事后钻研。
然后剩下的因为比较乱,都被我剪到evernote里了,查看「数据库」标签即可。
# 1. MySQL与InnoDB的架构
# 1.1 「数据库」与「数据库实例」
即database和instance的区别。
「数据库」是物理操作系统文件以及其他数据文件的集合。
「数据库实例」是数据库的后台线程以及共享内存的集合。
一般来说,实例与数据库是一一对应的,实例是操纵数据库的接口,没有实例便无法操作数据库。
# 1.2 MySQL架构(与InnoDB的关系)
借用draveness博客 (opens new window)的一张图。
数据查询的过程是这样:
- 连接器:客户端连接数据库
- 缓存中查找。
- 若缓存中不存在,则分析解析sql语句
- 优化器优化sql
- 利用存储引擎查询
InnoDB就属于存储引擎查询这一环。
# 1.3 数据如何存储(分级结构)
数据的存储是分级的:
- 表 tablespace 【.frm和.idb的区别】
- 段 segment
- 区 extent
- 页 page 【磁盘的最小操纵和管理单位】
- 行 row 【数据时按行存储的】
# 1.4 表的存储
表的定义在.frm文件中,表的数据和索引在.idb文件中。
.frm与引擎无关,任何一个平台或引擎都会存储表的定义在这个文件中。
数据也有两部分,一个是共享表空间,用于存放回滚信息,插入缓冲索引页等等数据,为公共区域。
另一部分为每张表的独立空间(需要开启),用于存放数据和索引等。
# 1.5 页的存储
页是 InnoDB 存储引擎管理数据的最小磁盘单位。每次查询行或者对行有任何操作,实际上都是把行所在的页读取到缓存汇总并进行操作。
# 1.6 行的存储
行有两种存储方式:Compact和Redundant,前者比后者能节省20%的空间。
行数据溢出时,会通过一个指针指向一个溢出页。
# 2. 索引
# 2.1 索引的目的与存在方式
索引的目的是为了快速查询。
InnoDB的索引使用B+树来建立,通常非叶子节点存储的都是索引值,叶子存储的是对应行数据(聚集索引)或者主键数据(辅助索引)。
但是需要注意的是,B+ 树索引并不能找到一个给定键对应的具体值,它只能找到数据行对应的页,然后数据库把整个页读入到内存中,并在内存中查找具体的数据行。
# 2.2 InnoDB为什么使用B+树
https://draveness.me/whys-the-design-mysql-b-plus-tree/
主要关注三种索引:哈希索引、B树索引和B+树索引的区别。
- 为什么不用哈希索引:哈希索引虽然能提供O(1)的单行操作性能,但是当面对范围查询与排序操作时,哈希索引无法很好的支持,而且会导致全表扫描。
- 为什么不用B树:B树虽然可以不必查找到叶子(可以再非叶子节点存储数据),但是在查询的过程中会有更多的随机IO,而B+树所有的数据都在叶子结点,而且用指针互相连接,这样一来可以通过顺序遍历查找,减少了随机IO(磁盘读取到内存的次数减少)。
# 2.3 聚集索引与辅助索引
首先聚集索引中存放的是(key,行数据),辅助索引存放的是(key,书签)。也就是说使用辅助索引并不能获取全部的行数据,而是得到一个书签,再依据这个书签再次查找(通常是再次在聚集索引中查找)。
聚集索引只有一个,辅助索引可以有多个。
# 2.4 联合索引与最左前缀匹配原则
联合索引是指对多个列进行索引。
联合索引的创建方法和单个索引的创建方法是一致的,不同之处仅在于有多个列。
最左前缀匹配原则,指的是在索引中会从左到右进行匹配,中间不允许中断。
举个例子,有一个表的联合索引为
key idx_a_b_c (a, b, c)
在这种情况下,下列情况和对应的联合索引使用情况:
- where a == x1 and b == x2 and c == x3; 使用联合索引(这时即使交换了顺序,优化器也会调整回来)
- where a == x1 and b == x2; 使用联合索引
- where a == x1; 使用联合索引
- where b == x2 and c == x3; 不满足最左前缀匹配原则
- where b == x2; 不满足最左前缀匹配原则
需要注意的是,即使不满足最左前缀匹配原则,也可能使用联合索引。这是因为不满足最左前缀匹配的话,可能会引发全表扫描,这时候可能恰好使用的还是联合索引,但是效率就没办法与正常的索引比较了。
# 2.5 全文检索
暂略
# 2.6 索引的设计
暂略
# 3. 锁
说实话,锁和事务这部分我感觉知识碎片化过于严重,因此采用前面提到的文章 (opens new window)中的脉络,加上自己的理解。
# 3.1 并发控制机制
有两种并发控制机制,即乐观锁和悲观锁:
- 乐观锁并不是锁,而是一种机制。每次更新数据都假设不会冲突,每次更新完后写回前都会判断原数据是否发生了变化。
- 悲观锁则是真正的锁,会认为一定会冲突,因此会申请锁。
当需要非常高的响应速度和并发量时选择乐观锁,当试错成本非常高时则选择悲观锁。
# 3.2 锁的种类
锁有两类,即:
- 共享锁(S)
- 排他锁(X)
其中共享锁类似读锁,排他锁类似写锁。前者互相兼容,而后者互不兼容。
# 3.3 InnoDB中锁的粒度
在InnoDB中,锁的粒度比较简单,只分表锁和行锁。
3.2节中说得两种锁指的就是行锁,而表锁称之为意向锁。
意向锁也分两种,即意向共享锁和意向排他锁,它们都是表示接下来我们将请求对该表中的某个行上锁。
**二者是相互兼容的,意向锁不会阻塞除了全表扫描以外的任何请求。**需要注意的是:
- IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
- 行级别的X和S按照普通的共享、排他规则即可。
意向锁的存在会让人觉得莫名其妙,其实它主要是针对表级的XS锁,例如下面这段话的总结:
有的人可能会对意向锁的目的并不是完全的理解,我们在这里可以举一个例子:如果没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;不过,在引入意向锁之后,当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向互斥锁(IX),再为行记录添加互斥锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了,只需要通过等待意向互斥锁被释放就可以了。
最后需要注意的是,当我们添加行锁的时候,意向锁是被自动添加的。
# 3.4 InnoDB中锁的算法
# Record Lock 记录锁
记录锁锁定的是索引记录。
【何时无法加行锁,并锁定整个表?】
# gap Lock 间隙锁
间隙锁锁定的是一个范围的索引记录,是用的最多的锁。
# Next-Key Lock 临键锁
Next-Key Lock是记录锁和间隙锁的结合,它锁定的是当前key所在的范围以及这个范围后的下一个范围。
【Next-Key Lock如何解决幻读问题?】
# 3.5 一致性非锁定读与一致性锁定读(涉及MVCC)
一致性非锁定读是利用MVCC实现的读取方式。读取操作不需要等待锁的释放,可是可以读取目标行的历史版本(即快照数据)。这是InnoDB引擎的默认读取方式。
快照数据是通过undo log完成的,一个行有不止一个快照数据,称这种技术为「行多版本技术」,由此带来的并发控制则称之为「多版本并发控制(MVCC)」。
对应上隔离级别,在READ COMMITTED和REPEATABLE READ下,InnoDB引擎使用的是一致性非锁定读。但是却有所不同。
- READ COMMITTED下,读到的是最新一份的快照数据。
- REPEATABLE READ下,读取到的是事务开始时候的行版本数据(也就是多次读到的都是这一份,保证了可重复读)。
一致性锁定读则是为了某些逻辑一致的要求。就是为了避免上面这种非锁定读,为了让读被阻塞而诞生的。
有两种语句支持一致性锁定读:
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
2
这两种情况下,前者会加一个X锁,后者加S锁。阻塞方式和前面讲的一样。注意这时候语句必须要在一个事务里,否则还是和一致性非锁定读一样会被直接读到数据。
# 3.6 死锁的发生与避免
死锁的原因和操作系统中是一样的:两个或两个以上的事务在执行过程中,因争夺锁而造成的一种互相等待的现象。
解决死锁:
最简单方式就是不要等待,一旦等待就回滚。
另一种方式就是设置超时机制。
除此之外还可以使用「等待图」的方式来检查死锁,这也是InnoDB采用的方式。
# 4. 事务
# 4.1 事务的概念与四大特性ACID
为了防止操作系统出现崩溃进而造成巨大问题,事务被引入了操作系统。
事务会把数据库从一种一致状态转换为另一种一致状态。要么整个转换成功,要么整个转换不保存(回滚到事务发生前的状态)。
事务有四个特性:ACID
A(atomicity, 原子性):原子性强调整个操作为一个不可分割的整体,要么进行完毕,要么不进行。
C(consistency, 一致性):一致性强调的是规则,是说事务发生前后,数据都是符合规则的,是合法的,是满足所有约束的。
I(isolation, 隔离性):隔离性也被称之为“并行控制”,“可串行化”,“锁”等,即第三节讲的锁机制实现了隔离性。
D(duranility, 持久性):事务一旦提交,其影响就是永久的,即使宕机数据库也能将其恢复。这保证了数据库的高可靠性。
# 4.2 隔离级别
隔离级别有4种,概念并不难懂,只是起名字的人非要起那么难懂,它们分别是:
- READ UNCOMMITED(读未提交)。意思就是,一个事务可以读到其他事务中未提交的数据。
- READ COMMITED(读提交)。意思是,一个事务只能读到其他事务提交后的数据。
- REPEATABLE READ(repeatable read,可重复读)。意思是,重复读的数据都一致(也就是不会重复读一个地方突然出现了不一致)。【InnoDB默认】
- SERIALIZABLE(serializable,串行化)。彻底串行,最高隔离级别。
# 4.3 几种特殊的“读”(脏读,不可重复读,幻读,丢失更新)
又是几个概念:
- 脏读:读到了其他事务中未提交的数据。
- 不可重复读:和第三级隔离刚好相反的意思,也就是重复读的时候发现多次读取的行数据变了。注意这里强调的是特定的行,而不是某个范围。
- 幻读:幻读的概念有些特殊,我总结为两种情况都算幻读,其主要偏重点是强调一个范围中被insert了新的行(这时与不可重复读的主要区别):
- 第一种,在隔离级别为READ COMMITED及以下时。(A事务)在一个范围中读取记录时,另一个事务往这个范围中插入了新的记录,导致A事务读到了这些记录,好像之前的查询是幻觉一样。
- 第二种,在隔离级别为REPEATABLE时,(A事务)在一个范围中读取记录时,另一个事务往这个范围中插入了新的记录,A再次读取时,由于可重复读隔离的原因,导致读到的还是原来的数据,但是A事务试图插入数据时,出现了key重复的问题。
- 丢失更新:是指一个事务的更新操作被另一个事务的更新操作覆盖。
这里有两个小问题
- 第一个问题是不可重复读和幻读有很大的概念上重合,要注意区分。
- 丢失更新不容易发生,在任何隔离级别上都不会发生这种问题,但是实际上在多用户计算机系统环境下是有可能发生这个问题的。详见《内幕》P275
# 4.4 事务的实现
事务的实现更多参考这篇文章 (opens new window)。
这里至少要能做到解释undo和redo的工作原理以及其解决的对应问题。
# 关于undo log回滚日志
undo log存储在磁盘中,是持久化的。它存储的是逻辑日志,是和已经发生的命令相反的逻辑,例如insert对应delete等。
# 关于redo log重做日志
redo log有两种,一种是缓存中的redo log,另一种是磁盘中的redo log。它存储的是物理格式日志,即哪一页被做了什么修改。相比于逻辑日志,物理格式日志的速度更快。redo log以512字节进行存储,因此保证了写入的原子性。
关于恢复的时机:每次存储引擎启动时,无论数据库上次是否正常关闭,都会尝试进行恢复。(LSN是什么)
redo log缓存向redo log磁盘文件刷新的时机:事务提交时。因此没有被提交的事务实际上是不能恢复的,redo log能恢复的是内存中尚未更新到磁盘的数据库数据。也就是说,redo log刷新的频率是比数据库数据高的,这样redo log才能起到作用。
【一个问题:redo可以回滚吗,undo可以重做吗?】
答:回滚日志可以重做(undo可以持久化),重做的过程当然可以回滚了。
【undo log和bin log的区别】