目录
引言
存储引擎和存储结构
两者的关系
存储结构
分类
1. 按数据组织方式分类
2. 按索引结构分类
3. 按存储介质分类
4. 按数据分布方式分类
5. 按数据冗余和备份分类
存储结构需要的特性
BTree
补充知识:Lock和Latch的区别(存储引擎并发操作和事务并发操作的不同)
Latch导致的性能损失
BTree的各种变种
LSM-Tree
补充知识:In-place和Out-of-place update方案差异
引言
数据库有三大模块:存储、事务、sql。
其中,存储模块负责数据在磁盘和内存上的存储、检索和管理,并向上层提供细粒度的数据操作接口。
因为存储和其他模块耦合较少,可以把它具象为一个专用的数据库组件,存储引擎。
存储引擎(Storage Engine)是数据库系统的组件,负责管理数据的物理存储和检索。在某些数据库系统中,存储引擎是可插拔的,这意味着可以选择或更换不同的存储引擎来适应不同的应用需求。存储引擎负责实际的数据存储、索引管理、事务支持、锁机制、崩溃恢复等底层操作。不同的存储引擎可能在这些方面有不同的实现方式,从而提供不同的性能和功能特性。
MySQL 是一个支持多种存储引擎的数据库系统。用户在创建表时,可以指定使用哪种存储引擎(如 InnoDB、MyISAM、Memory 等)。每个存储引擎都有自己的特点。
例如 InnoDB 支持事务和外键,
而 MyISAM 不支持事务但提供了更快的读操作。
根据应用场景的需求,用户可以选择最适合的存储引擎。
例如,
如果应用需要强事务支持和数据完整性保障,InnoDB 通常是首选;
如果需要高效的读操作和较少的写操作,并且不需要事务支持,MyISAM 可能更适合。
在 MySQL 中,一个数据库中的不同表可以使用不同的存储引擎。
例如,
一个数据库的用户表可以使用 InnoDB 存储引擎,以获得事务支持,
而日志表可以使用 MyISAM 存储引擎,以获得更高的写入性能。
对于存储而言,最重要的就是数据存储的结构(也即,数据结构!);
内存、缓存、读写流程的任何设计都是建立在存储结构的基础之上的!因此,存储结构和存储引擎的特性和性能关系非常密切!
存储引擎和存储结构
存储引擎和存储结构之间的关系可以类比为「软件」和「数据组织方式」的关系。存储引擎决定了数据的存储、检索、管理方式,而存储结构则是存储引擎在物理层面上实现这些功能所使用的具体数据组织形式。
存储引擎是数据库系统中负责管理数据存储和检索的核心组件,而存储结构则是存储引擎实现其功能的物理组织方式。存储引擎通过设计合理的存储结构来满足特定的性能要求和功能需求,两者密切关联,共同决定了数据库系统的数据管理效率和功能特性。
两者的关系
- 存储引擎实现存储结构:存储引擎负责实现和操作存储结构。换句话说,存储引擎通过设计和管理存储结构来实现其功能。
- 例如,InnoDB 存储引擎使用 B+ 树作为其索引结构,并使用数据页(通常为 16KB)作为基本的存储单位来管理表的数据。
- 存储结构为存储引擎服务:存储结构的设计必须服务于存储引擎的功能需求。
- 例如,InnoDB 的存储结构支持事务处理,因此它在存储结构中设计了事务日志(Redo Log 和 Undo Log)来支持事务的提交、回滚和崩溃恢复。
- 灵活性与优化:存储引擎通过灵活的存储结构设计来优化特定的操作。
- 例如,为了优化随机读写性能,存储引擎可能会使用聚簇索引(Clustered Index)将相关数据紧密存储在一起,减少磁盘的I/O操作。
存储结构
存储结构是指数据在物理存储介质(如磁盘、SSD)上的具体组织形式,用于支持数据库的高效存储、访问和管理。
分类
根据不同的应用需求和设计目标,存储结构可以分为多种类型。以下是存储结构的几种常见分类:
1. 按数据组织方式分类
-
堆存储(Heap Storage):
- 数据以无序的方式存储在表中,新插入的数据通常会被放置在表的末尾。
- 优点:插入操作较快,不需要维护数据的顺序。
- 缺点:数据检索速度较慢,通常需要全表扫描。
-
有序存储(Ordered Storage):
- 数据以某种顺序存储在表中,通常是按照一个或多个列进行排序。
- 优点:对于顺序查询或范围查询速度较快。
- 缺点:插入和删除操作较慢,需要维护数据的有序性。
-
聚簇存储(Clustered Storage):
- 数据按照主键或索引列的顺序存储,数据行与索引直接相关联。
- 优点:索引查找更快,因为数据和索引在物理上相邻。
- 缺点:插入、更新和删除操作可能较慢,因为需要维护数据和索引的顺序。
2. 按索引结构分类
-
B+树存储结构:
- 数据以 B+ 树的形式组织,常用于索引和快速数据检索。
- 优点:支持快速的等值查询和范围查询,树的高度较低,查找效率高。
- 缺点:插入和删除操作较复杂,可能需要调整树结构。
-
哈希存储结构(Hash Storage):
- 数据通过哈希函数映射到特定的存储位置,主要用于快速等值查询。
- 优点:等值查询速度非常快。
- 缺点:不适用于范围查询,哈希冲突可能导致性能下降。
-
全文索引结构(Full-Text Indexing):
- 专门为文本数据设计的索引结构,支持全文搜索。
- 优点:适用于快速全文检索。
- 缺点:创建和维护全文索引较为复杂,占用更多的存储空间。
3. 按存储介质分类
-
磁盘存储结构:
- 数据存储在磁盘介质上,常见于传统数据库系统。
- 优点:容量大,成本相对较低,数据持久化能力强。
- 缺点:I/O 操作较慢,随机访问速度不如 SSD。
-
内存存储结构(In-Memory Storage):
- 数据存储在内存中,常用于内存数据库或缓存系统。
- 优点:读写速度非常快,适合对延迟要求高的应用。
- 缺点:容量有限,数据持久化能力较弱,断电后数据可能丢失。
-
混合存储结构(Hybrid Storage):
- 数据同时存储在内存和磁盘中,结合两者优点。
- 优点:常用数据存储在内存中,提高访问速度,冷数据存储在磁盘中,降低成本。
- 缺点:设计复杂,需要平衡内存和磁盘的使用。
4. 按数据分布方式分类
-
行存储结构(Row-Oriented Storage):
- 数据按照行进行存储,每一行数据存储在一起。
- 优点:适合事务型应用(OLTP),对单行操作效率高。
- 缺点:对于列操作(如分析型查询,OLAP)效率较低。
-
列存储结构(Column-Oriented Storage):
- 数据按照列进行存储,每一列数据存储在一起。
- 优点:适合分析型应用(OLAP),对列操作效率高。
- 缺点:对单行操作效率低。
-
混合存储结构(Hybrid Storage):
- 结合行存储和列存储的优点,常见于现代数据仓库系统。
- 优点:兼顾事务型和分析型应用的需求。
- 缺点:设计和实现较为复杂。
5. 按数据冗余和备份分类
-
主从复制存储结构(Master-Slave Replication Storage):
- 数据通过复制机制存储在主库和从库中,主库负责写入,从库负责读取。
- 优点:提高读性能和数据可靠性。
- 缺点:数据写入有延迟,从库数据可能滞后。
-
分片存储结构(Sharding Storage):
- 数据水平分割存储在多个节点或服务器上,每个节点存储部分数据。
- 优点:支持大规模数据存储,提升写入和读取性能。
- 缺点:查询复杂性增加,需要跨节点协调。
存储结构需要的特性
我们有那么多的数据结构,例如:数组、链表、Hash表等,为什么 BTree 或 LSM-Tree 能够作为存储结构呢?
存储结构的共性特点:
- 适合磁盘存储(粒度一大),IO尽量少且一次读取连续的区域。
- 允许并发操作(粒度一小),增删改对存储结构的影响尽量小。
根据上面两个特性再审视一下之前上面列出的内存数据结构不难发现:
- 有1没2,即:只适合磁盘存储的,而不适合并发操作的:大文件、数组,他们都是一大段连续的存储区域,如果要修改,影响面很大,基本上要锁住整个数据结构!
- 有2没1,即:高并发但是不适合磁盘存储的:链表、哈希表、二叉树、红黑树等,他们的修改、写入影响小,但数据结构的粒度也非常小,一般一次只操作几个字节,不适合磁盘 IO;
那么是否存在数据结构同时满足上面两个特性呢?当然,BTree 就是这么个数据结构。
BTree
B树是一个以页为单位组织的。
首先,InnoDB 存储引擎中页的大小为 16k,一般可以指出几百上千个指针,因而具有高扇出、低高度的特点,从而保证了 B树是连续 IO、并且 IO 次数极少,因此适合磁盘存储;
其次,B树要修改的单位也是页,因此并发控制的锁在页上,B树并发的程度也很高。
但是并非这么简单,虽然 B树要修改的单位是页,但是B树存在 SMO(Structure Modification Operation) 操作,导致B树的并发能力并不高。
SMO(Structure Modification Operation) 操作:在增删改操作可能会造成节点的分裂或者合并,此时需要操作多个磁盘块!
所以,如果我们想要保证出现 SMO 操作时读写安全的话,就需要对有可能受到 SMO 操作影响的一整条链上所有节点加锁,如下图所示:
总之,虽然B树有一定的并发能力,但是由于 SMO 的存在使得B树的性能并不高,勉强满足并发要求,但是有很大的优化空间。
补充知识:Lock和Latch的区别(存储引擎并发操作和事务并发操作的不同)
这里补充一下存储引擎的并发操作和事务并发操作的不同。
假设都以锁机制来控制并发,上面两种机制对应的锁分别称为:Lock 和 Latch。
- Lock:用来维持数据库事务的 ACID 特性,事务级隔离,锁住的对象为用户的数据,是一个逻辑概念,例如:共享锁、互斥锁(S、X锁)等;
- Latch:保护数据读取过程中,加载到内存中数据结构的锁,是线程级隔离的锁;主要是防止多个线程并发去修改内存中的共享数据;
例如,假设修改一行数据,那么首先需要将这行数据所在的页加载至内存中,随后进行修改。
而在修改前,为了防止其他线程也要来修改这页数据,需要使用 Latch 对内存数据进行上锁;加好 Latch 后,可以对数据进行修改。
此时,为了防止在事务进行提交之前,存在其他别的事务读到这行修改后未提交的数据,此时需要对数据增加 Lock。
虽然 Latch 锁掉整个页数据,而 Lock 仅仅锁掉单行数据; 但是一旦完成了对这行数据的修改,那么 Latch 锁就可以释放,而 Lock 锁需要等到整个事务提交之后才能够释放! 在数据修改完成到事务提交的这段时间,Lock 就可以发挥作用了!
因此,在对数据库进行操作时,实际上是存在 Latch 和 Lock 两种锁共同生效的!但是对于用户而言,只能感觉到 Lock 锁(事务锁);
Latch导致的性能损失
虽然 Latch 的持续时间很短,但是他也会严重影响数据库性能!
BTree的各种变种
刚讲了上面的内容,那么 BTree 的各种变种的优化方向大致也就是通过优化下面两个方向来设计的:
- 1、适合磁盘存储(提高粒度),IO 尽量少且一次读取连续的区域;
- 2、允许并发操作(减小粒度),增删改对存储结构的影响尽量小;
例如,最出名的 B+Tree 在非叶子节点中仅保留指针(在 BTree 中非叶子结点也存储了行数据),所有的数据都存放在叶子节点,间接减少了树的高度。
并且这样还可以区分开索引段和数据段,有助于全表扫描时的顺序IO。总之,提升了 BTree 的特性1。
而 B-Link Tree 对 BTree 的并发控制机制做了很大的改进,提升了特性2。
对于近些年才提出的 Bw-Tree 而言,其采用了类似于 LSM-Tree 的 out-of-place update 方法,追加的写入完全无 Latch 操作(避免多线程并发写),从更根本的角度上提升了特性2。
LSM-Tree
LSM-Tree整体是一个分为多层、并且越向下层数据越多的层次树形结构;
对于写操作:
- 所有的写入操作都会首先直接写入内存(write),如果内存写满了,就会直接将内存中刚刚写入的这块数据刷入磁盘中(flush)。
- 当向磁盘刷入一定数量的数据之后,将本层数据和下一层数据进行合并、排序、去重。整理后的数据就放在下一层,因此就去除了数据的冗余(compaction)。
- Compaction 过程可能会持续多层,并且越下层的数据就越多,最终就形成了 LST-Tree 的整体结构。
读取的时候需要注意:由于一个 key 对应的 value 可能会存在于多个层次上,此时需要以最新(更上层的)数据为准。因此,在查询时需要从上至下一层层的搜索,而第一条找到的就是我们要读的结果!
由于其 Out-of-place 的特点:所以在正常插入到内存时,完全不会改变历史数据的结构,即:没有 SMO 过程。因此,并发能力很强,BTree 在特性2上的瓶颈在 LSM-Tree 中不存在。
而 LSM-Tree 中每层数据都比较多,在缓冲池没有命中时,读取 LSM-Tree 时读取的次数可能会非常多!所以,LSM-Tree 在特性1上的表现并不好!
但是,可以优化 LSM-Tree 的特性1,下面是几个常用的优化手段:
- 依靠 Compaction 操作整理 LSM-Tree 的结构,减少读 IO 的次数;
- 使用 Bloom Filter 对数据进行过滤;
实际上 Compaction 操作就可以看作为 LSM-Tree 的 SMO 操作!在 Compaction 期间,不仅会占用大量资源,并且还会造成缓冲丢失、写停顿(write stall)等问题,减少并发能力。因此,对 LSM-Tree 优化的关键点就落在 Compaction 操作上。
补充知识:In-place和Out-of-place update方案差异
对于 In-place update 结构而言,需要把磁盘中的结构加载到内存中,再修改并写回至磁盘中,因此免不了要使用 Latch 机制来做并发控制。
而对于 Out-of-place update 结构而言,虽然存在其他因素干扰其并发能力,但是由于所有的写入都是追加操作,因此无需采用基于 Latch 的机制进行并发控制。
由于现在多核处理器的发展,NUMA 模式逐渐成为主流,多核处理器在面对 Latch 的频繁获取和释放时都会损耗很多性能。
可以简单理解为,CPU 中每个核都有独立的一块存储区域,而读取或者写入的过程就需要将页数据加载到这块存储区域中。这样,并发读写时,一些节点就可能会存在多个核空间中。而加 Latch 和解 Latch 的操作又必须同步给多个核,这就造成了很大的性能损耗。
最显著的例子,对于 B+Tree 而言,在读写时由于根节点是必经之路,因此频繁的对根节点加解 Latch 就会极大的影响多核场景下的并发性能!