本文来自twt企业IT社区,作者/赵海。
很多数据库管理员可能对存储引擎并不熟悉,但接触MySQL以及其他一些NoSQL分布式数据库比较多的人可能对存储引擎就会深有感受。不同的存储引擎对数据的结构、数据的存储方式、数据的读取方式等都有不同的要求和特点。存储引擎的基本思想是决定具体数据库产品的适用场景的最根本原因,本文希望通过这些原理性的讨论和分析展示给大家有一个宏观的视图,从而指导具体的数据库设计实践。
1.什么是数据库的存储引擎技术
数据库的存储引擎是什么?它主要解决什么问题?
很多数据库管理员可能对存储引擎并不熟悉,因为大多数常见关系型数据库基本只有一种存储引擎,没有给我们选择和设计的机会,例如Oracle、SQL Server。但是如果我们接触MySQL以及其他一些NoSQL分布式数据库比较多的人可能对存储引擎就会深有感受。首先,我们认为存储引擎就是为了实现数据存储以及数据检索而实现的解决方案,如何建立索引,如果实现更新,如何检索数据等都是它的功能实现范畴。常见的存储引擎有哈希存储引擎和树存储引擎,树存储引擎又分为B-Tree、B+Tree、LSM-Tree等若干种。不同的存储引擎对数据的结构、数据的存储方式、数据的读取方式等都有不同的要求和特点。
2.不同存储引擎如何建立索引
2.1 B-Tree
B树数据结构其实是在我们大学当中所学数据结构课程当中的二叉树基础上的一种升级和改进。最早是由德国计算机科学家Rudolf Bayer等人于1972年在论文《Organization and Maintenance of Large Ordered Indexes》提出。
如图所示,B树事实上是一种平衡的多叉查找树,也就是说最多可以开m个叉(m>=2),我们称之为m阶b树。总的来说,m阶B树满足以下条件:
(1)每个节点至多可以拥有m棵子树。
(2)根节点,只有至少有2个节点(极端情况,就是一棵树就一个根节点)。
(3)非根非叶的节点至少有Ceil(m/2)个子树(图中5阶B树,每个节点至少有3个子树)。
(4)非叶节点中信息包括[n,A0,K1,…,Kn,An],其中n表示该节点保存的关键字个数,K为关键字且Ki(对应到关系型数据库当中的信息,就是二位表当中记录的主键信息)。
(5)从根到叶子的每一条路径都有相同的长度,也就是指向这些节点的指针为空。
2.2 B+Tree
B+树实际上是B-Tree的升级版,它是基于原有数据结构的不足支持进行系列改造之后形成的存储引擎技术,如图所示:
从图中所示的状况我们可以很直观感受到:B+树与B树最大的不同是所有数据记录都保存在叶子节点中,叶子结点是有指针将所有数据连接起来的。具体来说,B+树与B树的主要区别:
(1)有n棵子树的节点含有n个关键字(也有认为是n-1个关键字);
(2)所有的叶子节点包含了全部的关键字,及指向含这些关键字记录的指针,且叶子节点本身根据关键字自小而大顺序连接;
(3)非叶子节点可以看成索引部分,节点中仅含有其子树中的最大或最小关键字.
由于采用了这样的结构,B+树对比B树有以下两方面优点:
首先,索引节点上由于只有索引而没有数据,所以索引节点上能存储比B树更多的索引,这样树的高度就会更矮。那么查询的时间复杂度就会更低。再有,因为数据都集中在叶子节点了并且叶子节点增加前后指针,指向同一个父节点的相邻兄弟节点,给范围查询提供遍历。而如果使用B树结构,由于数据既可以存储在内部节点也可以存储在叶子节点,范围查询是很繁琐的。
2.3 Hash
哈希存储引擎的数据库本身只是一个健值存储系统,数据库当中存储的数据以文件的物理形式表现,但是每一个物理文件当中存储的具体数据内容主要包含两种:一种是主健,另外一种是具体的数据值。用户通过put(key,value)来写入数据,或者通过get(key)接口来获取数据,每条记录都是一个健值对。
哈希索引本身有很多种实现方式,有基于静态哈希实现的索引结构,也有基于动态哈希实现的索引结构,其具体的实现方式依赖于不同的数据库。一般来讲,哈希索引表的结构如图所示:
我们知道HashMap<K,V>,可以通过K来获取V,对于以上的哈希索引来说,PrimaryKey就是我们要取得的V值。比如(PK=key mod 2)叫做散列函数或者哈希函数,那么PK的区间范围,我们称其为散列地址。存储的时候通过散列函数算出散列地址,然后把value的值存入,查找的时候通过散列函数算出散列地址,然后读出数据。
3.不同存储引擎的数据检索
3.1 B-Tree&B+Tree
对于基于二叉树数据结构基础之上形成的树的存储结构,那么其查询数据最核心的算法就是二分法查找算法,即通过键值的比较排除一定比例的可能性,从而缩小数据查找的范围,最终通过几次比较定位到要查找的数据。直观表现期间,我们还是以图为例:
按照图示,我们查找的数据是L,
首先,将根节点的数据块从磁盘读入内存,读出P值,比较发现小于P;
接着,遍历根节点左指针指向数据块,读出C、F、J、M值,顺序比较后,找到J&M之间的指针;
最后,遍历指针指向数据块,读出K、L值,定位所查询的数据L。
根据以上算法,那么本次查找经历了三次磁盘的读取,三次内存数据的比较。由此看见,B树检索的时间复杂度主要取决于树的深度以及节点内保存的数据数量的多少。
对于B+树的检索,其实算法与B树非常类似,其主要区别在于B+树的所有检索操作都不会在非叶子节点结束,每一个检索都会经历相同的长度,那就是从根节点到叶子节点,途中经历的非叶子节点只保留索引信息,只有叶子节点才会保留所有键值数据。这种算法把所有遍历的复杂度留在了叶子节点的扫描上,减少了检索途中的IO次数,保证了叶子节点扫描的局部优势,同时也保障了所有检索操作的时间复杂度相对的稳定性。因此大部分关系型数据库选择的是B+树作为其存储引擎。
3.2 Hash
对于Hash存储引擎的数据检索,我们首先要聊到它的增、删、改操作。
数据文件分活跃状态和陈旧状态两种。
数据的增加操作,用户写入的记录直接追加到活动文件,因此活动文件会越来越大,当到达一定大小时,活跃的数据文件会被冻结。引擎重新建立一个活跃文件用于写入,而之前的活跃文件则变为陈旧的数据文件。写入记录的同时还要在索引哈希表中添加索引记录。
数据的删除操作,用户不直接删除记录,而是新增一条相同Key的记录,把Value值设置一个删除的标记。原有记录依然存在于数据文件中,然后更新索引哈希表。这样的话,在处理检索操作的时候,用户就会最先读到哈希索引表里面的空值记录,原有记录后续处理。
数据的更新操作,不支持随机写入。对于存储系统的基本功能中更新,实际上和增加数据操作都是一样的,都是直接写入活动数据文件。同时修改索引哈希表中对应记录的值。这个时候,实际上数据文件中同一个Key值对应了多条记录,根据时间戳记录来判断,以最新的数据为准。
数据的读取操作,读取时,首先从索引哈希表中定位到记录在数据文件中的具体位置,然后通过读取出对应的记录。当然在读取索引表的时候,索引的结构有可能是索引树结构,在检索索引的过程当中会有一定的复杂度,具体根据树的深度来判断检索的复杂度。
4.不同存储引擎技术的选择设计
4.1 B&B+树的优劣势分析
首先,通过对B树和B+树的检索算法特点来看,从使用的角度上来说,B树索引存储结构多用于OLTP型的数据库,因为这类数据库主要以事务,或是行级别的读取和存储为主的(比如MYSQL)。换言之,这种类型的数据库更多的操作是小批量或单行级别的随机读取和更新,并且还有事务方面的需求。在此前提条件之下,之所以有B+树的诞生,取决于以下两点:a.磁盘读写代价更低。B树的内部结点并无指向关键字具体信息的指针。所以其内部结点相对B树更小。若是把全部同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的须要查找的关键字也就越多。相对来讲IO读写次数也就下降了。b.B+树只要遍历叶子节点就能够实现整棵树的遍历。并且在数据库中基于范围的查询是很是频繁的,而B树实现这样的操作需要的代价非常高,效率非常低。
其次,通过对B树和B+树的更新和删除算法特点来看,虽然算法相对哈希及其他存储引擎实现的算法来讲会显得非常复杂,代价很高。但是从另外一个侧面来看,正是由于它没有通过大量的数据追加实现更新和删除,它就无需去管理那些不同时间戳版本的重复数据。有效地利用了磁盘空间和内存空间,这个也与我们OLTP型关系型数据库的规模以及通用服务器硬件的配置非常匹配。
4.2 Hash的优劣势分析
首先,从数据结构特点来看。我们在前边提到了它的数据结构以及索引表的结构,我们发现最大的特点就是在于所有的这些数据结构都是以模式为基础的。所以基于这一点来看,它本身更适合能以键值对的模式表示的数据存储,无论是固定的键值,还是变动的键值。其次,我们来分析哈希存储引擎索引表检索算法的特点。如果冲突处理的算法的当,它大概率可以通过一次哈希函数就可以定位到数据的基本位置,与B-Tree存储引擎相比较而言,它少了树根、树枝、树叶节点的遍历和多次的读取操作。从哈希存储引擎添加、删除、更新数据的算法特点来看,基于除了检索之外所有的数据操作都是通过添加新数据来变相实现。同样与B-Tree存储引擎相比较而言,添加一条新的纪录远比检索、加锁、修改、放锁这个过程要效率很多。从这个意义上来讲,如果我们能把这些符合键值对要求的索引表数据全部引入到内存,那么对于随机读取的并发能力提升无疑是巨大的质变,这也是它能被Redis、Memcache这类内存数据库选中的重要原因。最后,所以对于事务性要求不是非常强,并且包含大量写入及更新的数据场景就比较有优势了。
矛盾总是贯穿于事物的发展过程当中,有利就有弊。对于哈希存储引擎也是如此,正是因为它的优势而导致了一些不可避免的劣势。首先、由于哈希存储引擎的数据结构特点,那么对于一些数据内部字段之间以及数据本身有着相对复杂的关系的数据,比如二维表数据。哈希存储引擎就会束手无策。其次,由于哈希存储引擎的检索算法是基于哈希索引表的哈希函数计算实现,那么它就只能实现一次比较孤立的数据定位,对于范围的查询以及检索过程当中的一些排序、分组、过滤等操作就力不从心了。最后,还是从其数据增加、删除、更新的算法来看。它是牺牲了大量的存储空间来实现操作的高效性,那么后续必然会带来空间的管理代价以及数据的合并处理代价,数据片越大、哈希树的高度越高,那么数据检索的效率相应会提高很多,因为哈希函数定位之后必然随之而来的是对定位到的数据片的全部扫描,数据片越大,检索的平均效率越差。同时后台执行的数据片合并的时间越长。因此对于数据粒度比较大,又没有一个好的哈希函数可以使用的场景,也不是哈希存储引擎使用的最佳场景。
5.总结与展望
无论是树还是哈希存储引擎,它们都是数据存取技术的设计思想,很多关系型数据库大多基于B-Tree家族实现的,而很多分布式NoSQL数据库都是基于Hash家族实现的,在每一种产品具体实现的过程中可能会改进其中的一些算法细节从而实现部分缺陷的优化,尤其是一些开源的数据库。但是这种存储引擎的基本思想是决定具体数据库产品的适用场景的最根本原因,本文希望通过这些原理性的讨论和分析展示给大家有一个宏观的视图,从而指导具体的数据库设计实践。当然也希望更多同业能从更多维度继续分析讨论并分享。