1. 存储引擎简介
接触数据库比较深的人可能对存储引擎并不陌生,但是大多数数据库管理员可能对存储引擎并不熟悉,因为我们大多数关系型数据库的学习者并不需要关心存储引擎的选择和应用,因为我们熟知的 Oracle 、 SQL Server 等数据库基本只有一种存储引擎,没有给大家选择和设计的机会。但是如果我们接触 MySQL 以及其他一些 NOSQL 数据库比较多的人可能对存储引擎就会深有感受。
那么存储引擎是什么?它主要解决什么问题呢?
首先,我们认为存储引擎就是如何为了解决存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。用户可以根据应用的需求选择更适合自己应用场景的存储引擎来提高数据库的性能和数据的访问效率。常见的存储引擎有哈希存储引擎、 B 树存储引擎、 LSM 树存储引擎等三种。不同的存储引擎对数据的结构、数据的存储方式、数据的读取方式等都有不同的要求和特点。今天我们要跟大家分享的是其中的一种所谓的哈希存储引擎。哈希存储引擎是一种利用哈希表的持久化实现对数据进行增、删、改以及随机读取的技术。利用哈希存储引擎的常见数据库有 Redis , Memcache 。
2. Hash 存储引擎基本架构
2.1 Hash 存储引擎的存储格式
哈希存储引擎的数据库本身只是一个 KV 存储系统,数据库当中存储的数据以文件的物理形式表现,但是每一个物理文件当中存储的具体数据内容主要包含两种:一种是主健,另外一种是具体的数据值。用户通过 put(key,value) 来写入数据,或者通过 get(key) 接口来获取数据,每条记录都是一个 key-value 。那么对于哈希表记录的具体数据格式如表 2.1.1 所示:表 2.1.1 哈希记录格式File-ID :数据库存储记录的主健所在的物理文件的唯一编号。Value-Lenth :数据具体长度。Value-Position :数据偏移位置。Time-Stamp :主健对应的最新的数据的时间戳(同一个主健对应的具体值可能有多个版本)。表 2.1.2 数据记录格式CRC :数据校验位。Time-Stamp :数据值版本对应的具体时间戳。Key-Length :主健长度。Value-length :数据记录值的长度。Key-Data :主健的具体值。Value-Data :数据记录的具体值,值的具体格式及内容根据不同数据库要求,可以有多种形式。通过这样的数据结构,内存基于哈希表的索引数据结构,通过主键快速定位到数据。哈希表中的每一项包含三个用于定位数据的信息, File-ID , Value-Position , Value-Length 。通过读取 File-Id 对应文件的 Value-Position 开始的 Value-Length 个字节,得到数据的具体值。
2.2 Hash 索引的存储格式
哈希索引本身有很多种实现方式,有基于静态哈希实现的索引结构,也有基于动态哈希实现的索引结构,其具体的实现方式依赖于不同的数据库。一般来讲,哈希索引表的结构如表 2.2.1 所示:表 2.2.1 哈希索引表我们知道 HashMap< K,V> ,可以通过 K 来获取 V, 对于以上的哈希索引来说, PrimaryKey 就是我们要取得的 V 值。比如 ( PK = key mod 2 ) 叫做散列函数或者哈希函数,那么 PK 的区间范围,我们称其为散列地址。存储的时候 通过散列函数算出散列地址,然后把 value 的值存入,查找的时候 通过散列函数算出散列地址 ,然后读出数据。在存储的过程当中,会有这样的情况发生:PK = key1 mod 2 = key2 mod 2 。也就是说会有两个或者是多个 Key 通过散列函数计算得出的结果一样,这个时候我们称之为冲突。如果我们的哈希函数不够合理,如果我们的 Key 值足够多,那么这个冲突的概率就会越大,以同一个 PK 值为索引值的数据片段就会非常大,在数据片段里面再去寻找具体值的效率就会非常低下,那么有没有办法解决这个问题呢,这就是接下来要说的哈希树存在的必然价值。首先我们来看哈希树的结构,如图 2.2.1 :
图 2.2.1 哈希索引树哈希树的建立遵从“质数分辨定理”,如图,按照指数序列,一般都是从 2 开始,即第一层的结点有 2 个子节点,第二层结点每个节点都有 3 个子节点,第三层每个节点有 5 个子节点,第四层结点每个节点都有 7 个子节点,第五层每个节点有 11 个子节点,第六层结点每个节点都有 13 个子节点,以此类推。这样的话,按照这样的思路,我们来看 key 值的插入:首次, Hashmap ( key1 )插入根节点;二次, Hashmap ( key2 )前面插入冲突,那么计算 Hashmap2 ( key2 , 3 ),插入二层节点;三次, Hashmap ( key3 )又前面插入冲突,那么计算 Hashmap3 ( key3 , 3 , 5 ),插入三层节点;最终,在一个数据片段上可能会有很多( KV )插入,但是经过这样的数据结构,这些经过哈希冲突了的数据也能有序地在数据片段上存储,并且检索的时候也会有序。这样就在一定程度上解决了冲突带来的数据存取效率问题。对于哈希存储引擎来讲,其处理冲突的方法有很多,包括线性探测法、二次探测法、双哈希函数探测法以及链地址法等。不同的数据库产品对于其最终的实现也会有自己具体细节的改进和更新,需要根据具体数据库产品特点进行探究。
3. Hash 存储引擎数据操作原理
3.1 Hash 存储引擎的数据操作原理
说到存储引擎的数据操作,主要包括数据的增、删、改、查操作。再谈及这些操作之前,我们需要了解哈希存储引擎对于数据文件的管理模式,如图 3.1.1 所示:
图 3.1.1 哈希存储引擎数据库文件结构数据文件分活跃状态和陈旧状态两种。数据的增加操作,用户写入的记录直接追加到活动文件,因此活动文件会越来越大,当到达一定大小时,活跃的数据文件会被冻结。引擎重新建立一个活跃文件用于写入,而之前的活跃文件则变为陈旧的数据文件。写入记录的同时还要在索引哈希表中添加索引记录。数据的删除操作,用户不直接删除记录,而是新增一条相同 Key 的记录,把 Value 值设置一个删除的标记。原有记录依然存在于数据文件中,然后更新索引哈希表。这样的话,在处理检索操作的时候,用户就会最先读到哈希索引表里面的空值记录,原有记录后续处理。数据的更新操作,不支持随机写入。对于存储系统的基本功能中更新,实际上和增加数据操作都是一样的,都是直接写入活动数据文件。同时修改索引哈希表中对应记录的值。这个时候,实际上数据文件中同一个 Key 值对应了多条记录,根据时间戳记录来判断,以最新的数据为准。数据的读取操作,读取时,首先从索引哈希表中定位到记录在数据文件中的具体位置,然后通过读取出对应的记录。当然在读取索引表的时候,索引的结构有可能是索引树结构,在检索索引的过程当中会有一定的复杂度,具体根据树的深度来判断检索的复杂度。
3.2 Hash 存储引擎的数据文件操作原理
哈希存储引擎的增删改查,我们在上一节内容介绍过了。其中一个非常重要的特点就是底层数据文件的操作只有追加没有减少和修改,所有的更新和删除都是通过覆盖间接实现,这样的话势必造成数据文件不断膨胀的问题,那么哈希存储引擎如何解决这个问题呢。这就是我们下面要说的合并操作。合并 (Marge) 操作就是为了剔除膨胀的这部分数据,减小数据文件大小。合并操作通过定期将所有陈旧文件当中的数据进行扫描生成新的数据文件。如果同一个 Key 有多条记录,则只保留最新的一条,从而去掉数据文件中的冗余数据。而且进行合并操作时,还可以顺带生成线索文件。所谓的线索文件就是数据文件当中非常特殊的一部分,它里面存储的数据结构与数据文件非常相似,区别在于其不存具体数据值,取而代之的是数据值得具体位置,这样的话,我们就可以通过扫描较小的线索文件新建或者更新内存当中的哈希索引表。最终以较小的扫描代价换取哈希索引视图的快速建立。
4. Hash 存储引擎优略分析
4.1 Hash 存储引擎的优势
其实关于哈希存储引擎的优劣分析,我们是希望进一步得到其最适合以及最不适合的使用场景,从而指导我们的实践。同样关于存储引擎的优缺点,追根溯源还是要找到其本身的数据结构特点、组织架构特点以及算法特点。首先,我们来分析哈希存储引擎的数据结构特点。我们在前边提到了它的数据结构以及索引表的结构,我们发现最大的特点就是在于所有的这些数据结构都是以 模式为基础的。所以基于这一点来看,它本身更适合能以键值对的模式表示的数据存储,无论是固定的键值,还是变动的键值。其次,我们来分析哈希存储引擎索引表检索算法的特点。如果冲突处理的算法的当,它大概率可以通过一次哈希函数就可以定位到数据的基本位置,与 B-Tree 存储引擎相比较而言,它少了树根、树枝、树叶节点的遍历和多次的读取操作。所以它的检索效率是非常高的。从这个意义上来讲,如果我们能把这些符合键值对要求的索引表数据全部引入到内存,那么对于随机读取的并发能力提升无疑是巨大的质变,这也是它能被 Redis 、 Memcache 这类内存数据库选中的重要原因。最后,我们来分析哈希存储引擎添加、删除、更新数据的算法特点。基于除了检索之外所有的数据操作都是通过添加新数据来变相实现。同样与 B-Tree 存储引擎相比较而言,添加一条新的纪录远比检索、加锁、修改、放锁这个过程要效率很多。所以对于事务性要求不是非常强,并且包含大量写入及更新的 数据场景就比较有优势了。
4.2 Hash 存储引擎的劣势
矛盾总是贯穿于事物的发展过程当中,有利就有弊。对于哈希存储引擎也是如此,正是因为它的优势而导致了一些不可避免的劣势。我们还是从其特点说起。首先、由于哈希存储引擎的 数据结构特点,那么对于一些数据内部字段之间以及数据本身有着相对复杂的关系的数据,比如二维表数据。哈希存储引擎就会束手无策。对于一些虽然可以表示成为键值对模式,但是键值之间的关系并没有一个好的哈希函数可以利用的数据,也显得那么苍白无力。其次,由于哈希存储引擎的检索算法是基于哈希索引表的哈希函数计算实现,那么它就只能实现一次比较孤立的数据定位,对于范围的查询以及检索过程当中的一些排序、分组、过滤等操作就力不从心了。由于其检索的时候利用的是索引表的全局数据视图来进行哈希计算,也就是说它的哈希地址不能局部使用,那么对于一些希望利用局部特性完成检索的场景就毫无意义了。最后,还是从其数据增加、删除、更新的算法来看。它是牺牲了大量的存储空间来实现操作的高效性,那么后续必然会带来空间的管理代价以及数据的合并处理代价,数据片越大、哈希树的高度越高,那么数据检索的效率相应会提高很多,因为哈希函数定位之后必然随之而来的是对定位到的数据片的全部扫描,数据片越大,检索的平均效率越差。同时后台执行的数据片合并的时间越长。因此对于数据粒度比较大,又没有一个好的哈希函数可以使用的场景,也不是哈希存储引擎使用的最佳场景。
5. 总结展望
哈希存储引擎是一种在 NOSQL 数据库当中经常会用到的数据存储引擎技术,尤其是一些内存类数据库。具体数据库在其实现及使用哈希存储引擎的过程当中,涉及的可能不仅仅是文中所述的这些关于数据特点、数据操作特点方面的策略,可能还会涉及到缓存的算法、数据的持久化以及其他的一些方面的策略选择。本文希望通过这些原理性的讨论和分析展示给大家一个基本视图,也希望各位同业针对更多更深层次的内容进行发展性研究探讨并分享。
【 参考文献】
[1] Kai Ren, Qing Zheng, Joy Arulraj, Garth Gibson: A Space-Efficient Key-Value Storage Engine For Semi-Sorted Data
[2] https://mongoing.com/?s=storageEngine
[3] https://www.storageengine.com/
[4] https://mariadb.com/kb/en/memory-storage-engine/