存储引擎
- 存储引擎就是存放和读取用户数据的地方,对于持久化的存储引擎而言,数据的归宿是非易失性的存储介质(通俗意义上来说就是磁盘)所以该以什么形式组织和存储数据,这就是存储引擎设计的艺术所在
- 这一块涉及到和操作系统打交道(主要是IO操作),还有如何更快的处理数据,这里涉及到并发事物如何处理,另外考虑空间局部性和时间局部性原理,这里涉及到对数据缓存的设计
- 存储引擎是存储系统的发动机,提供数据的增、删、读、改能力,直接决定存储系统的功能和性能
常用存储引擎数据结构
这篇文章主要罗列的是Hash,B+Tree,LSM-Tree三种存储引擎的资料。
Hash存储引擎
哈希存储引擎是哈希表的持久化实现,支持增、删、改,以及随机读取操作,但不支持顺序扫描,不支持排序,不支持范围查询,时间复杂度O(1), 对应的存储系统为键值(Key-Value)存储系统,Redis 就是使用的Hash 存储。
常用哈希函数
- 除留余数法:H(Key)=key % p (p ≤ m)p最好选择一个小于或等于m(哈希地址集合的个数)的某个最大素数
- 直接地址法: H(Key) =a * Key + b;“a,b”是常量。
冲突处理方法
- 开放地址法: 如果两个数据元素的哈希值相同,则在哈希表中为后插入的数据元素另外选择一个表项
- 线性探测法: 这种方法在解决冲突时,依次探测下一个地址,直到有空的地址后插入,若整个空间都找遍仍然找不到空余的地址,产生溢出
- 链地址法: 将哈希值相同的数据元素存放在一个链表中,在查找哈希表的过程中,当查找到这个链表时,必须采用线性查找方法
B 树存储引擎
- B树存储引擎是B树的持久化实现,不仅支持单条记录的增、删、读、改操作,还支持顺序扫描(B+树的叶子节点间的指针)
- 相比哈希存储引擎,B树存储引擎不仅支持随机读取,还支持范围扫描
- 大量的随机写会产生大量的随机写IO
B-树存储
- 所有结点都存储数据,搜索有可能在非叶子结点结束
- 关键字集合分布在整颗树中,任何一个关键字出现且只出现在一个结点中
- 非叶子节点存储了data数据,导致数据量很大的时候,树的层数可能会比较高,随着数据量增加,IO次数的控制不如B+树优秀。
- MongoDB 存储结构 正是使用了 B-树
B+树存储
LSM树存储引擎
- 数据库的数据大多存储在磁盘上,无论是机械硬盘还是固态硬盘(SSD),对磁盘数据的顺序读写速度都远高于随机读写。大量的随机写,导致B树在数据很大时,出现大量磁盘IO,速度越来越慢,基于B树的索引结构是违背上述磁盘基本特点的—需较多的磁盘随机读写。
- 于是,基于日志结构的新型索引结构方法应运而生,主要思想是将磁盘看作一个大的日志,每次都将新的数据及其索引结构添加到日志的最末端,以实现对磁盘的顺序操作,从而提高索引性能。对海量存储集群而言,LSM树也是作为B+树的替代方案而产生。
- HBase,LevelDB,RocksDB这些NoSQL存储都是采用的LSM树
LSM树架构
LSM树有以下三个重要组成部分
-
MemTable:MemTable是在内存中的数据结构,用于保存最近更新的数据,会按照Key有序地组织这些数据,因为数据暂时保存在内存中,内存并不是可靠存储,如果断电会丢失数据,因此通常会通过WAL(Write-ahead logging,预写式日志)的方式来保证数据的可靠性
-
Immutable MemTable:将转MemTable变为SSTable的一种中间状态。写操作由新的MemTable处理,在转存过程中不阻塞数据更新操作
-
SSTable(Sorted String Table):有序键值对集合,是LSM树组在磁盘中的数据结构。为了加快SSTable的读取,可以通过建立key的索引以及布隆过滤器来加快key的查找
-
执行写操作:先同时写memtable与预写日志WAL。memtable写满后会自动转换成不可变的(immutable)memtable,并flush到磁盘,形成L0级sstable文件。sstable即有序字符串表(sorted string table),其内部存储的数据是按key来排序的
-
执行读操作时:会首先读取内存中的数据,即active memtable→immutable memtable→block cache。如果内存无法命中,就会遍历L0层sstable来查找。如果仍未命中,就通过二分查找法在L1层及以上的sstable来定位对应的key
Redis存储原理剖析
Redis是用C语言开发的一个开源的高性能键值对(key-value)内存数据库
数据存储原理
redis 中以redisDb作为整个缓存存储的核心,保存着我们客户端需要的缓存数据
- dict和expires是redisDB中最主要的两个属性,分别保存了对象数据键值对和key的过期时间,其底层数据结构都是dict字典。之所以分开存储,由于过期时间并不是数据的固有属性,虽然分开存储需要两次查找,但是却能节省内存开销。
- blocking_keys和ready_keys主要为了实现BLPOP等阻塞命令
- watched_keys用于实现watch命令,记录正在被watch的一些key
- eviction_pool是记录可能被lru淘汰的一些备选key
- id为当前数据库的id,redis支持单个服务多数据库,默认有16个
数据持久化方式-RDB
- Redis将内存中的数据库快照保存到磁盘中。redis服务器在挂掉重启之后,可以通过加载RDB文件进行数据的恢复。
- 优点:对Redis性能影响低;二进制压缩文件体积小;数据恢复块;缺点:有数据丢失可能,备份文件版本兼容问题
数据持久化方式-AOF
- 将Redis写操作命令及参数保存到AOF文件中
- AOF 文件持久化的功能分成三个步骤,文件追加(append),文件写入,文件同步(sync)。
- AOF 文件在写入磁盘之前是先写入到 aof_buf 缓冲区中,然后通过调用 flushAppendOnlyFile 将缓冲区中的内容保存到 AOF 文件中。
- 优点:AOF只是追加日志文件,因此对服务器性能影响较小,速度比RDB要快,消耗的内存较少
- 缺点:AOF方式生成的日志文件太大,即使通过AFO重写,文件体积仍然很大。恢复数据的速度比RDB慢。
MySQL存储原理分析
整体架构
- MySQL 在整体架构上分为 Server 层和存储引擎层。
- 其中 Server 层,包括连接器、查询缓存、分析器、优化器、执行器等,存储过程、触发器、视图和内置函数都在这层实现。
- 数据引擎层负责数据的存储和提取,如 InnoDB、MyISAM、Memory 等引擎。在客户端连接到 Server 层后,Server 会调用数据引擎提供的接口,进行数据的变更。
- 连接器:负责和客户端建立连接,获取用户权限以及维持和管理连接。可以用 show processlist; 来查询连接的状态
- 查询缓存:当接受到查询请求时,会现在查询缓存中查询(key/value保存),是否执行过。没有的话,再走正常的执行流程。MySQL8.0 已经删除这一部分
- 分析器:词法分析:如识别 select,表名,列名,判断其是否存在等。语法分析:判断语句是否符合 MySQL 语法
- 优化器:确定索引的使用,join 表的连接顺序等,选择最优化的方案。
- 执行器:使用数据引擎提供的接口,进行查询。
存储原理
数据库文件结构
- MySQL 数据库文件由多个文件组成,其中最重要的是数据文件(.ibd 或 .frm 文件)和日志文件(.ib_logfile* 文件)。数据文件存储表数据和表索引,而日志文件存储事务日志和二进制日志。
存储引擎
- MySQL 支持多种存储引擎,例如 InnoDB、MyISAM、Memory 等。其中 InnoDB 是 MySQL 默认的存储引擎,它支持事务、行级锁等高级特性,而 MyISAM 则不支持事务和行级锁,但具有更高的性能。
数据存储
- 在 InnoDB 存储引擎中,表数据和表索引都是以 B+ 树的形式存储的。每个 B+ 树节点都包含多个数据行或索引行,其中数据行存储表数据,索引行存储表的索引。
- 在数据存储方面,InnoDB 对数据行进行了细致的管理和优化。每个数据行都包含一个固定长度的行头和可变长度的数据区。行头包括事务 ID、行格式、行长度等信息,而数据区包括实际存储的数据。此外,InnoDB 还通过行版本控制(MVCC)来支持事务隔离级别和多版本并发访问。
索引存储
- 在 InnoDB 存储引擎中,表索引采用 B+ 树结构存储。B+ 树是一种多路平衡树,它能够快速定位数据行或其他索引行。B+ 树节点包含多个数据行或索引行,其中数据行存储表数据,索引行存储索引键和指向下一级 B+ 树节点的指针。
- 在索引存储方面,InnoDB 对索引行进行了细致的管理和优化。每个索引行都包含一个固定长度的行头和可变长度的索引键。行头包括事务 ID、行格式、行长度等信息,而索引键则是用于定位数据行的关键字。
日志存储
- 在 MySQL 中,事务日志是一种非常重要的组件,它用于记录所有数据库更改操作。事务日志分为两种类型:redo log 和 undo log。
- redo log 记录了所有数据库更改操作的物理信息,例如插入、更新和删除操作。redo log 是循环写入的,当写满时会覆盖最早的日志记录。这是因为 MySQL 通过重放 redo log 来恢复崩溃时未提交的事务。
- undo log 记录了所有数据库更改操作的逻辑信息,例如回滚事务操作。undo log 是先写入,后删减的,它记录了每个事务操作的逆向操作,以便在回滚事务时使用。
事务处理
- MySQL 支持 ACID 属性的事务处理,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。在事务处理方面,MySQL 使用多版本并发控制(MVCC)机制来实现隔离性和多版本并发访问。
- MVCC 机制通过给每个事务分配一个唯一的事务 ID,并记录每个数据行的创建和删除时间戳来实现多版本并发访问。当一个事务需要读取数据时,MySQL 会根据当前系统时间戳和数据行的时间戳来判断该数据行是否对该事务可见。如果数据行的时间戳早于该事务的时间戳,则该事务无法读取该数据行。
当一个事务需要修改数据时,MySQL 会将该数据行的旧版本保存到 undo log 中,并在新版本上执行修改操作。如果该事务回滚,则 MySQL 可以使用 undo log 来还原该数据行的旧版本。 - 隔离级别
- Read Uncommitted(读取未提交内容):最低隔离级别,会读取到其他事务未提交的数据,脏读;
- Read Committed(读取提交内容):事务过程中可以读取到其他事务已提交的数据,不可重复读;
- Repeatable Read(可重复读):每次读取相同结果集,不管其他事务是否提交,幻读
- Serializable(可串行化):事务排队,隔离级别最高,性能最差;
高可用与备份
数据库主从复制
MySQL 主从复制是指数据可以从一个MySQL 数据库服务器主节点复制到一个或多个从节点。
- 从库通过手工执行 change master to 语句连接主库,提供了连接的用户一切条件(user 、password、port、ip),并且让从库知道,二进制日志的起点位置(file名 position 号)start slave;
- 从库的 IO 线程和主库的 dump 线程建立连接;
- 从库根据 change master to 语句提供的 file 名和 position 号,IO 线程向主库发起 binlog 的请求;
- 主库 dump 线程根据从库的请求,将本地 binlog 以 events 的方式发给从库IO 线程;
- 从库 IO 线程接收 binlog events,并存放到本地 relay-log 中,传送过来的信息,会记录到 master.info 中;
- 从库 SQL 线程应用 relay-log,并且把应用过的记录到 relay-log.info 中,默认情况下,已经应用过的 relay 会自动被清理 purge。
MySQL 主从复制是MySQL本身就支持的,其优势为:
- 主从复制是mysql自带的,无需借助第三方。
- 数据被删除,可以从binlog日志中恢复。
- 配置较为简单方便。
但是也存在一些问题,比如:
- 从库要从binlog获取数据并重放,这肯定与主库写入数据存在时间延迟,因此从库的数据总是要滞后主库。
- 对主库与从库之间的网络延迟要求较高,若网络延迟太高,将加重上述的滞后,造成最终数据的不一致。
- 单一的主节点挂了,将不能对外提供写服务。需要我们手动切换。
LevelDB 存储原理分析
LevelDB 是 Google 开发的一种高性能键值存储系统,它使用类似于 LSM(Log-Structured Merge-Tree)的存储原理来实现高效的数据访问和更新
leveldb中主要由以下几个重要的部件构成:
- memtable:一个在内存中进行数据组织与维护的结构
- immutable memtable:不可修改的memtable
- log(journal):写内存之前会首先将所有的写操作写到日志文件中
- sstable:将数据持久化到磁盘中。除了某些元数据文件,leveldb的数据主要都是通过sstable来进行存储。
- manifest:用来记录记录版本修改信息
- current:这个文件的内容只有一个信息,就是记载当前的manifest文件名
存储原理
数据存储
- LevelDB 中的数据存储在一个或多个 SSTable(Sorted String Table)中,每个 SSTable 包含多个数据块。SSTable 中的数据按照键进行排序,以便支持范围查询和迭代访问。
- 每个数据块都包含一个索引和多个数据项。索引是一个跳表(Skip List),它可以快速定位数据项。数据项包含一个键和一个值,它们都以字节数组的形式存储。
索引存储
- LevelDB 中的索引存储在内存中的一个 MemTable 中,MemTable 是一个跳表,它可以快速定位键值对。当 MemTable 达到一定大小时,LevelDB 会将其转换为一个 SSTable,然后创建一个新的 MemTable。
- 为了支持快速的键值访问和范围查询,LevelDB 中的索引使用了前缀压缩编码(Prefix Compression Encoding)。前缀压缩编码可以将相邻键的前缀部分压缩为一个前缀块,以减少索引的存储空间和提高查询性能。
数据访问
- LevelDB 中的数据访问主要分为两种模式:读取和写入。在读取模式下,LevelDB 首先从内存中的 MemTable 中查找键值对,如果没有找到则从磁盘中的 SSTable 中查找。在写入模式下,LevelDB 会将键值对写入内存中的 MemTable 中,并将其异步写入磁盘中的 SSTable 中。
- 为了支持高效的范围查询,LevelDB 使用了一个基于 SSTable 的迭代器(Iterator)。迭代器可以在多个 SSTable 中进行迭代,以支持范围查询和逆序查询。
合并和压缩
- 为了保持数据存储的高效性,LevelDB 会定期进行 SSTable 的合并和压缩。
- 合并操作将多个小的 SSTable 合并成一个大的 SSTable,以减少磁盘空间的占用和提高查询性能。
- 压缩操作将多个旧的 SSTable 压缩成一个新的 SSTable,以减少磁盘空间的占用和提高查询性能。
并发控制
- leveldb中采用了MVCC来避免读写冲突。
- 试想一下,当某个迭代器正在迭代某个sstable文件的内容,而后台的major compaction进程完成了合并动作,试图删除该sstable文件。那么假设没有任何控制并发的机制,就会导致迭代器读到的内容发生了丢失。
- 最简单的处理方式就是加锁,当发生读的时候,后台所有的写操作都进行阻塞,但是这就机制就会导致leveldb的效率极低。故leveldb采用了多版本并发控制的方法来解决读写冲突。具体体现在:
- sstable文件是只读的,每次compaction都只是对若干个sstable文件进行多路合并后创建新的文件,故不会影响在某个sstable文件读操作的正确性;
- sstable都是具有版本信息的,即每次compaction完成后,都会生成新版本的sstable,因此可以保障读 写操作都可以针对于相应的版本文件进行,解决了读写冲突;
- compaction生成的文件只有等合并完成后才会写入数据库元数据,在此期间对读操作来说是透明的,不会污染正常的读操作;
- 采用引用计数来控制删除行为。当compaction完成后试图去删除某个sstable文件,会根据该文件的引用计数作适当的删除延迟,即引用计数不为0时,需要等待至该文件的计数为0才真正进行删除;
文档参考
- https://bbs.huaweicloud.com/blogs/197482
- Building a Log-Structured Merge Tree in Go
- Go存储引擎资料分享
- leveldb-handbook
- https://github.com/syndtr/goleveldb
- MySQL高可用集群方案优劣对比