吾日三省吾身:高否? 帅否? 富否?
<center> 阿浪:否!否!否!</center>
<center>o ( ̄ヘ ̄o#)</center>
所谓人丑就要多读书,阿浪一直对 MySQL 数据存储的底层结构比较模糊,经过几天几夜的奋发读书 (摸鱼),终于让我拨开了萦绕心头的困惑。
# 存储引擎
我们都知道 MySQL 拥有多种存储引擎。MEMORY、MRG_MYISAM、
CSV、FEDERATED、PERFORMANCE_SCHEMA、MyISAM、InnoDB、BLACKHOLE、ARCHIVE 等,但其中最常用的就是 MyISAM 和 InnoDB。
# MyISAM 引擎
在 MySQL 5.5 之前的版本,MyISAM 是默认的存储引擎。读性能良好,拥有较高的查询速度。
不支持行级锁,只支持表级锁。因为 MyISAM 会一次性获得 SQL 所需的全部锁 (这句话包含的信息很多),所以不会出现死锁,这少了很多麻烦事。当 MyISAM 在执行 select/insert/update/delete 语句时,会自动给涉及的表加读锁 (共享),在执行对表做结构变更操作时,会加写锁 (独占)。
当写锁和读锁同时被申请时,优先获得写锁。读锁之间不互斥,读写锁之间、写锁之间是互斥的,通俗的讲,读操作不会阻塞其他线程对同一表的读请求,但会阻塞对同一表的写请求,而写操作会全部阻塞。由于写锁可能会一直阻塞读锁,所以这正是表级锁发生锁冲突概率最高的原因。
本篇文章主要介绍 InnoDB 的存储,就不对其它知识点做过多拓展。
总结: 不支持行级锁、不支持事物、支持表级锁、读性能较高。
# InnoDB 引擎
从 MySQL 5.5 版本开始,官方选择 InnoDB 作为 MySQL 默认的存储引擎。既支持行级锁,也支持表级锁,但默认情况下是采用行级锁。当 InnoDB 在执行 select 语句时,不会加任何锁,在执行 insert/update/delete 操作时,会自动给涉及数据行加排它锁。
InnoDB 支持外键,包含外键的 InnoDB 表转为 MyISAM 会失败。InnoDB 最大的不同就是支持事物,所有的存储引擎中只有 InnoDB 是事务性存储引擎,对每一条 SQL 语言都默认封装成事务自动提交。
而且,InnoDB 支持 MVCC。目前,随着行业发展项目数据的吞吐量越来越大,支持 MVCC 对于高并发是一个很大的优势。
总结: 支持行级锁、支持表级锁、支持事物。
更直观的比较:
综上所述,两者各有各的优势,如何选择应该看自己项目的场景。如果表中绝大多数都只是读查询,可以考虑 MyISAM,如果既有读也有写,使用 InnoDB 效果更好。
# 页 (Page Structure)
大学学习操作系统的时候,我们知道 "页" 是操作系统从磁盘取数据到内存的基本单位,默认为 4KB。虽然部分处理器会使用 8KB、16KB 或者 64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流。
CPU 在做指令操作时,从目标地址取数据到内存需要一次 IO 操作,多次操作数据就要相应发生多次 IO。若每次取的数据都很小 (远小于 4KB),为减少 IO 操作每次直接取一页 (4KB)。
# InnoDB 中的 “页”
在 MySQL 中,select 指令根据 where 字段查找数据时,表中每行数据都要被读到内存中依次与之比较,有多少条数据就要发生多少次 IO 操作。这样资源消耗太大,于是 InnoDB 借鉴了操作系统对内存的管理,引入了 “Page Structure” 来减少 IO 操作。
InnoDB 采用” 页 “的形式存储用户数据,有时也被称为 “块”。MySQL 默认的非压缩数据页为 16KB,0~16KB 偏移量即为 0 号数据页,16KB-32KB 的为 1 号数据页,依次类推。
从 MySQL 官网可以了解到,InnoDB 的页结构包含以下内容:
因为 Page 的内部结构很复杂,阿浪只能带着大家从宏观上浅层了解,感兴趣的同学可以对着官方文档深入研究:
https://dev.mysql.com/doc/internals/en/innodb-page-structure.html
<font size=4 color=red>Fil Header</font>
fil header 用来描述页信息。记录了页的空间 ID,区分了记录在同一文件内所属不同表空间的数据页,并且提供指针连接各个数据页。
其中最重要的是 fil_page_prev 和 fil_page_next 两个指针,当表数据量达到一定规模时,就会生成大量的数据页,这时候需要指针通过双向链表的形式维护数据页的连接性和有序性。
<font size=4 color=red>Page Header</font>
用于记录页内的状态信息。分配新页的信息、页内第一条数据地址等。
<font size=4 color=red>User Records</font>
表内有多少条数据,UserRecords 就要存储多少条记录。若是主键,存储完整的表数据,并根据主键升序排列来提升检索效率。若是非主键索引,则根据索引字段排序。
<font size=4 color=red>Page Directory</font>
UserRecords 内可能是一个很长的单链表,因为长链表不支持随机访问,检索时间随着数据规模的增长而增长。所以 PageDirectory 以组划分链表记录用户数据的相对位置,默认以 6 条数据为一组。
当查询数据时,直接与 Page Directory 内的数据比较快速锁定数据范围,然后进入 UserRecodes 获取完整数据。
画图看更直观 (简图):
# 物理划分上的 “区” “段”
区 (extent):由连续的 64 个 16KB 的 Page 组成的大小为 1M 的空间。区是实际的 1M 物理空间。当碎片区用完之后,就会申请 1M 的连续物理空间。
段 (segment):由一个或多个区组成,不要求是地址连续的区。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
我从网上找到一张图,很形象的展示了区在表空间内的结构。
# B-Tree 与 B+Tree
# 传统的 B-Tree
有些人会读成 “B 减树”,虽然只是个名称,但出于规范化考虑,还是推荐把 “-” 看成连接符,读作 “B 树”。
说到 B 树可能日常中不常用,但二叉查找树 (Binary Search Tree) 大家应该很熟悉。对于它的每个节点有这么个特性,若左子树不空,则左子树上所有结点的值均小于它的根结点的值。若右子树不空,则右子树上所有结点的值均大于它的根结点的值。
在二叉查找树的基础上又产生了平衡二叉查找树 (Self-Balancing Binary Search Tree),增加了 左子树与右子树的高度之差的绝对值不超过 1 的特性,大名鼎鼎有的 AVL 树和红黑树。下面是一颗 AVL 树:
B 树就是在平衡二叉树基础上实现的多路平衡查找树,继承了二叉平衡树的所有特征,又由于自身是多叉树,所以是个 “小胖墩”。下面是一颗最小度数为 5 的 B 树:
<font size=4 face="华文楷体"> 我们很明显的看出来 B 树比 AVL 树的高度要小,这会带来什么好处呢?</font>
如果数据都放在磁盘里,与内存访问时间相比,磁盘访问时间非常长。大多数树操作需要 O (h) 次磁盘访问,其中 h 是树的高度。由于 B 树的高度较低,因此与二叉平衡树 (如 AVL 树、红黑树等) 相比,大多数操作的总磁盘访问量显着减少。
B 树的特征 (m 阶):</br>
1)每个结点最多存储 m-1 个关键字。</br>
2)根结点最少可以存储 1 个关键字。</br>
3)非根结点至少有 Math.ceil (m/2)-1 个关键字。
了解 B 树后,我们知道了从根节点到叶子节点的连接特性,但这样会损失叶子节点也可以相互指向带来的效率。InnoDB 允许在叶与叶之间相互指向,所以 InnoDB 内被称为 B + 树。
# InnoDB 中的 B+Tree
其实 B + 树仅仅只是 B 树的一个变种,而 InnoDB 中的 B + 树又在此基础上做了一个小改进。下面是最大度数为 4 的 B + 树:
与上面的 B 树对比,最明显的是叶子结点通过指针相互连接,只看叶子结点的话就是一条单链表。为何说 InnoDB 里的 B + 树是传统 B + 树的改良版呢?因为 InnoDB 里的 B + 树的叶子结点不只有后继指针,还有多出来的前驱指针,叶子结点构成了一条双向链表。
双向指针使得