不同的数据库存储系统都会设计不同的索引结构来优化查询/写入效率, 在讨论这些结构之前, 我们先从头回顾一下计算机存储的一些设计
计算机的存储器设计采用了一种分层次的结构。寄存器、高速缓存、主存和硬盘,从顶至底,这些存储器的速度逐级递减而容量逐级递增,并且伴随越来越低的价钱,如图
在现代计算机里面, 上面的存储实际上分为CPU(寄存器,高速缓存L1、L2、L3)、内存、硬盘(SSD,HDD)
局部性原理(Principle of Locality)指在程序执行过程中,倾向于访问某些局部特定的数据或指令,而不是随机地访问整个内存空间。通常分为两种类型:时间局部性和空间局部性。
系统调用的read/write和底层的硬盘读写之间存在一层I/O缓存(Kernel buffer cache),在Linux中称为Page Cache,它利用了局部性原理来提高系统的I/O性能:
Page Cache
中。如果这个数据页在近期内被再次访问(时间局部性),那么可以直接从 Page Cache 中读取,而无需再次访问硬盘。Page Cache
中,以便后续的访问。Page Cache的大小是根据当前系统的可用内存和工作负载动态调整的,此外还会通过页面置换算法如 LRU 定期淘汰旧的数据页。Page Cache可以大大减少硬盘I/O,从而提高系统的性能。
Page Cache支持写回(write-back)和写穿(write-through)两种策略:
Linux下默认使用 write-back
策略,即文件操作的写只写到 Page Cache 就返回。Page Cache中被修改的内存页称之为脏页(Dirty Page)
,脏页在特定的时候被一个叫做pdflush(Page Dirty Flush)
的内核线程写入硬盘,写入的时机和条件如下:
顺序I/O(Sequential I/O)是一种数据访问模式,其中数据按照连续的顺序进行读取或写入。这与随机I/O(Random I/O)形成对比,随机I/O是指数据的访问位置在存储设备上是随机分布的。 顺序I/O的性能之所以高,主要是因为它能够最大化利用存储设备的局部性原理,并且减少了寻道时间和旋转延迟:
内存访问速度和硬盘访问速度的对比结果。简单总结下:
访问速度对比:
存储引擎是存储系统的发动机,决定了存储系统的性能和功能。存储引擎主要负责数据如何读写,包括读多写少和写多读少场景,读取操作又分为随机读取和顺序扫描。目前常见的存储引擎使用的存储数据结构主要有:
mysql 在指定索引的时候可以使用 B+Tree
或 HashTable
。
由于哈希映射关系,哈希表在查找单条数据时候,能保证O(1)的时间复杂读。但哈希表在范围查询和排序遍历时候,只能进行全表扫描并依次判断是否满足条件,这样就会让性能影响非常大, 所以不是特殊场景(譬如 Memory 引擎)不会选择 HashTable 当做底层存储结构
相较于 HashTable 查找的平均时间复杂度比O(1)稍慢的是O(logn),这类数据结构有 **平衡二叉树(AVL Tree),红黑树(RB Tree)、B树(B Tree)、B+树(B+ Tree)**等。此类型结构天然就支持排序、范围查找操作。
以平衡二叉树为例,一般情况下查询性能非常好,但平衡二叉树单个节点只能保存一个数据,因此当覆盖大量数据时候,平衡二叉树的树高度很高。这意味着当需要通过遍历获取存储在硬盘上的数据时候,需要更多次的I/O操作。硬盘读取时间远远超过数据在内存中比较的时间,这将导致程序大部分时间会阻塞在硬盘 I/O 上。
为了降低树的高度, 减少 I/O 操作, 可以选择多叉树。
B树是一种多叉树, 每个节点都存放着索引和数据, 相比与平衡二叉树, 每个节点能够容纳更多的键值数据, 减少树的高度和硬盘访问次数。
B+树是B树的一种变种,它与B树的区别是: 叶子节点保存了完整的索引和数据,而非叶子节点只保存索引值。所以查询索引数据,必须要遍历到叶子节点才能取到,因此查询时间固定为 O(logn)。 另外一点是, B+tree 的叶子节点会有一个指针指向下一个叶子节点, 这让所有的叶子节点用单链表的方式连通了起来, 这样对于范围查询来说非常方便, 能够最大程度的利用 pagecache 和预读机制
相较于B Tree, B+ Tree 的高度会更矮, 范围查询的时间也会稳定一些。
相比于数据读取场景,B+树在写多场景的性能并不理想。这主要原因是:B+树在插入和删除操作时,可能需要进行节点的分裂和合并,以保持树的平衡。这会导致更新硬盘上多个数据页和Page Cache页缓存数据失效,这些操作伴随大量的随机I/O,限制B+树的写入效率。
RocksDB 的核心数据结构被称为日志结构合并树 (Log Structured Merge Tree,LSM Tree),LSM树是一种专为写密集型工作负载设计的数据结构。
如上图, LSM树主要分为四个部分:
LSM 写入流程如上图:
由于 SSTable 不可修改,在不同的 SSTable 中,可能存在相同 Key 的记录,当然最新 SSTable 的那条记录才是准确的。这样的设计虽然大大提高了写性能,但同时也会带来一些问题:
LSM 读取流程如上图:
在极端情况下,如果我们要搜索的数据根本就不存在。那这种情况下,我们需要读取完所有的数据才能返回给上层用户,搜索的数据不存在。
当数据量较大时,这种查找过程效率极低。往往在工程上实现时,会采用布隆过滤器来加速读取,查找每个 SSTable 时,首先会根据布隆过滤器来拦截一次,如果布隆过滤器检测当前的查找的数据不存在,那么查找的数据就一定不存在当前的SSTable中,加快读取效率。不过布隆过滤器存在误判情况,所以在实际使用时,需要合理的设置布隆过滤器的几个关键参数。
LSM树引入了合并(Compaction)机制,可以降低空间放大和读放大,但代价是更高的写放大(Write Amplification),即实际写入操作数量大于真正的写入操作数量。 在合并操作中,每个被合并的 SSTable 中的数据都需要被读取出来,然后写入到新的 SSTable 中。因此,一个数据项可能会被多次写入到硬盘中.
写放大会增加硬盘I/O,尤其是在业务高峰期,从而降低系统的性能。因此LSM树提供了不同合并策略在空间、读和写放大之间进行权衡,这种策略主要包括
从上面的读写流程, lsm 的核心设计思想(要解决的问题)如下:
可以看到每个问题 lsm tree 都抽象了一些模块来解决:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有