算法基础——存储

news/2025/2/3 0:37:27 标签: 算法, 大数据, 存储

引入

基础理论的进步,是推动技术实现重大突破,促使相关领域的技术达成跨越式发展的核心。

在发展日新月异的大数据领域,基础理论的核心无疑是算法。不管是技术设计,还是工程实践,都必须仰仗相关算法的支持,才能够真的落地应用。

下面我们就看看大数据相关领域有哪些核心的算法

存储算法

大数据存储相关的核心算法,主要是为了高效存储和管理海量数据,以及提升数据读写性能和存储利用率等。

以下我们来看看大数据领域最经典的存储算法

B树及其变种(B+树、B* 树)

原理

  • B树:是一种自平衡的多路搜索树,每个节点可以有多个子节点。它的所有叶子节点都在同一层,并且包含了所有的数据。
  • B+树:是 B树的一种变种,它的非叶子节点只存储索引信息,所有的数据都存储在叶子节点中,叶子节点之间通过指针相连,形成一个有序链表,便于范围查询。(最通用)
  • B*树:在 B+树的基础上,对节点的分裂规则进行了优化,提高了空间利用率。

应用场景:广泛应用于关系型数据库的索引结构,能够高效地支持点查询和范围查询。

优点:查询、插入和删除操作的时间复杂度都是 O (log n),性能稳定。

缺点:对于大规模数据的写入操作,可能会导致频繁的节点分裂和合并,影响性能。

B+树是一种平衡的、多叉的树形结构,能够支持O(logn)的插入和查询时间复杂度。B+树的整个结构是有序存储的,这使得B+树能够高效地支持范围查询;在空间放大维度,B+树能够达到70%的空间利用率。综上所述,B+树有较好的综合性能,在现代的诸多存储系统中,B+树索引很常见,例如关系数据库MySQL的默认存储引擎InnoDB。

大数据领域是避免不了使用多线程与高并发场景的,所以需要对B+树索引进行并发控制。由于B+树的树形结构会不断动态调整,要实现一个正确的多线程B+树,存在着较大的设计挑战。

目前来说,实现B+树的并发,可以采用以下3种机制:

  1. 锁耦合
    锁耦合机制是B+树中应用最为广泛的一种加锁方式。锁耦合机制就是一种节点级别的加锁方式,但是路径上的节点的锁会更早地释放,同时能保证线程安全。在锁耦合机制中,每个线程同时最多拥有两个节点的锁,分别为父节点和孩子节点。父节点的节点可以在孩子节点的锁获取之后释放,这样可以充分减少每个节点加锁和释放的临界区大小,从而最大化多线程性能。
  2. 乐观锁机制
    采用锁耦合机制,每个读/写线程仍然是互相阻塞的,而乐观锁机制则是为了减少写线程对读线程的阻塞,并进一步减少加锁的数量。内部节点除节点内部的锁字段之外,还额外维护一个写版本号。每当写线程对节点完成修改之后,先对写版本号完成自增操作,随后释放写锁。每当读线程访问一个节点的时候,首先记录节点版本号,在完成对节点的访问之后检测节点版本号是否发生变化,如果节点写版本号发生变化,读线程重做对该节点的访问,否则意味着节点访问过程中该节点并未发生写操作,因此读节点操作成功执行。
  3. 无锁机制。
    通过无锁的方式来操作B+树,提升随机读和范围查询的性能。它的核心的思想是把B+树的页(page)通过page id(PID)映射到map,map的[key,value]变成[PID,page value]​,把直接对page的修改,变成一个修改的操作记录,加入到“page value”​。所以“page value”可能是一个“base page”​,即page原始的内容,和一串对page修改形成的记录的链表,而在修改记录链表中加入一个修改记录节点可以很容易变成一个无锁的方式来实现。另外,对B+树的split和merge操作也通过类似的原理,把具体的操作细化成好几个原子操作,避免传统的加锁方式。

SkipList(跳表)

原理

  • SkipList 是一种可以用来快速查找数据的数据结构,它基于有序链表,并通过在链表节点上增加多层索引来提高查找效率。在 SkipList 中,每个节点都可能有多个指针,这些指针指向不同层次的下一个节点,高层的指针可以跳过更多的节点,从而加快查找速度。
  • 构建 SkipList 时,会按照一定的概率随机决定每个节点在不同层次出现的概率。例如,一个节点可能以 50% 的概率出现在第一层,以 25% 的概率出现在第二层,以 12.5% 的概率出现在第三层,以此类推。这样就形成了一个类似金字塔形状的多层结构,使得查找操作可以在对数时间内完成。

应用场景:由于 SkipList 在插入、删除和查找操作上都具有较高的效率,适合在内存中存储和操作大量的有序数据,能够快速地根据分数对元素进行排序和查找;在分布式哈希表(DHT)等分布式数据结构中,SkipList 可以用于实现节点之间的快速路由和数据查找。通过在不同节点上构建 SkipList 结构,可以高效地定位数据所在的节点,提高分布式系统的性能和可扩展性。

优点:SkipList 的插入、删除和查找操作的平均时间复杂度为 O (log n),与平衡树(如红黑树)等数据结构相当,但 SkipList 的实现相对简单,代码复杂度较低,易于理解和维护。而且 SkipList 支持动态扩展和收缩,能够方便地适应数据量的变化。

缺点:SkipList 的空间复杂度相对较高,因为每个节点可能包含多个指针,需要额外的空间来存储这些指针。此外,由于 SkipList 的节点层数是随机生成的,在极端情况下可能会出现查找性能下降的情况,但这种情况发生的概率较低。

跳跃表(SkipList)是一种能高效实现插入、删除、查找的内存数据结构,这些操作的期望复杂度都是O(logN)。与红黑树以及其他的二分查找树相比,跳跃表的优势在于实现简单,而且在并发场景下加锁粒度更小,从而可以实现更高的并发性。正因为这些优点,跳跃表广泛使用于KV数据库中,诸如Redis、LevelDB、HBase都把跳跃表作为一种维护有序数据集合的基础数据结构。

LSM树(Log-Structured Merge Tree)

原理:将数据的写入操作先记录在内存中(通常是一个有序的数据结构,如跳表),当内存中的数据达到一定阈值后,再批量地将数据写入磁盘,形成一个有序的数据文件(SSTable,Sorted String Table)。磁盘上的数据会按层级进行组织,不同层级的数据会定期进行合并操作,以减少数据冗余和提高查询效率。

应用场景:适用于写多读少的场景,如日志存储、时间序列数据存储等。

优点:写入性能高,能够快速处理大量的写入请求;

缺点:读取时可能需要合并多个 SSTable,读取性能相对较低,并且在合并过程中会产生一定的 I/O 开销。

2000年年初,Google发表了Bigtable的论文,论文中的创新点之一就是它所使用的文件组织方式,即LSM树。

算法的核心也是基于硬件特性来,才能真正的解决落地的问题。对于磁盘读写来说,顺序读写要远比随机读写快,LSM树通过将随机写转化为顺序写,消去随机的本地更新操作来提高写入性能,但查询(包括点查询和范围查询)性能会有一定程度的下降,因为一次查询操作可能要遍历磁盘中的许多个不同的SST文件。针对查询性能问题,在不同应用实现时会有一些优化,比如在HBase中设计了异步的compaction来降低文件个数,来提高读取性能。

LSM树本质上和B+树一样,是一种磁盘数据的索引结构。但和B+树不同的是,LSM树的索引对写入请求更友好。因为无论是何种写入请求,LSM树都会将写入操作处理为一次顺序写。

LSM树的索引一般由两部分组成,一部分是内存部分,一部分是磁盘部分。内存部分一般采用跳跃表来维护一个有序的KeyValue集合。磁盘部分一般由多个内部KeyValue有序的文件组成。

哈希算法(Hash Tables)

原理:通过哈希函数将键映射到一个固定大小的数组中,数组中的每个位置称为一个槽(Slot)。当插入、查找或删除数据时,先计算键的哈希值,然后根据哈希值找到对应的槽。如果发生哈希冲突(即不同的键映射到了同一个槽),可以采用开放寻址法、链地址法等方法来解决。

应用场景:适用于快速查找和插入的场景,如缓存系统、分布式哈希表(DHT)等。在分布式系统中,一致性哈希算法是一种常用的哈希算法,用于实现数据的均匀分布和节点的动态扩展。

优点:平均查找、插入和删除操作的时间复杂度为 O (1),性能高效;

缺点:哈希函数的设计比较关键,如果哈希函数设计不当,可能会导致哈希冲突频繁,影响性能。并且哈希表不支持范围查询。

哈希表是一种无序的数据结构,它提供了快速的插入操作和查找操作。

一个好的哈希表能够保证插入和查找的时间复杂度为O(1),即插入和查询性能与哈希表中的数据量无关。这种设计可以实现高效的写性能和查询性能,但是它牺牲了范围查询性能。

哈希表结构设计中最关键的问题是:

  1. 如何选择合适的哈希函数;
  2. 如何选择合适的哈希冲突处理机制。

常见的哈希冲突解决机制有四种:

  1. 链地址法。
    在链地址法下,哈希表的每个桶由一个链表构成。链表中存储的是所有哈希值相同的键值对。因此在进行查询操作时,可以通过遍历该链表查询对应的键值对。
  2. 线性探测法。
    在线性探测法下,哈希表是一个连续的桶数组,对于任意一个哈希键,根据哈希函数定位到一个映射位置,插入和查找都基于该地址进行向后探测。当插入一个键值时,判断映射地址是否为空,如果该地址为空,则在映射地址插入键值对,否则向后探测直到找到空桶,并将该键值对放入该空桶。查询操作则从映射地址开始向后扫描所有键值对,直到找到待查询键值对或者遇到一个空桶。
  3. 双选择法。
    双选择法采用两个独立的哈希函数,对于每个键值对,都有两个可插入的桶。当执行插入的时候,根据两个哈希函数分别将哈希键映射到两个桶a和b中。根据桶a和桶b的填充度,选择填充度更低的桶插入键值对。同样,执行查询操作时,只需要遍历两个桶即可定位到查询键值。
  4. 布谷鸟探测法。
    布谷鸟探测法是双选择法的一种变种。它同样采用两个哈希函数。当执行键值对插入时,根据两个哈希函数分别将哈希键映射到两个桶a和b中。如果桶a和b存在空闲位置,则将键值对插入到空闲位置中;否则,随机挑选一个桶中的键值对,将其踢出该桶,并存入待插入键值对,被踢出的键值对则尝试插入到其对应的另一个桶中。

采用不同哈希冲突解决方式,在查询性能、插入性能、哈希表填充度三个维度会有不同的表现,解决哈希冲突的方案也是没有“银弹”。

链地址法的插入性能更优,并且对于空间的占用是逐渐增长的;线性探测法的填充度可以做到最优,但是这是以牺牲查询和插入性能为前提的;在查询性能上,布谷鸟和双选择法会比其他方法更优。在实际的键值数据库中,不同的设计会采用不同的哈希函数和哈希冲突解决机制。Redis采用的就是链地址法,这使得Redis的空间占用更为缓慢,空间管理也更为灵活。

LRU(Least Recently Used)和 LFU(Least Frequently Used)缓存算法

原理

  • LRU:基于 “最近最少使用” 的原则,当缓存空间满时,优先淘汰最近最少使用的数据。通常使用双向链表和哈希表来实现,双向链表用于维护数据的访问顺序,哈希表用于快速查找数据。
  • LFU:基于 “最不经常使用” 的原则,当缓存空间满时,优先淘汰使用频率最低的数据。可以使用多个链表和哈希表来实现,每个链表存储相同使用频率的数据。

应用场景:常用于缓存系统中,如数据库缓存、Web 服务器缓存等,以提高数据的访问速度。

优点

  • LRU:实现简单,能够较好地反映数据的访问局部性。
  • LFU:能够更好地适应数据的使用频率。

缺点

  • LRU:对于某些特殊的访问模式,可能会导致性能下降。
  • LFU:实现相对复杂,并且在数据访问模式发生变化时,需要一定的时间来调整。

总结

今天提到的都是存储相关最核心的算法,本文主要是抛砖引玉,后续在分享大数据相关组件底层实现原理时,有涉及到相关算法的时候,我们再深入看看。


http://www.niftyadmin.cn/n/5840400.html

相关文章

自动化测试框架搭建-封装requests-优化

目的 1、实际的使用场景,无法避免的需要区分GET、POST、PUT、PATCH、DELETE等不同的方式请求,以及不同请求的传参方式 2、python中requests中,session.request方法,GET请求,只支持params传递参数 session.request(me…

tf.Keras (tf-1.15)使用记录4-model.fit方法及其callbacks参数

model.fit() 方法是 TensorFlow Keras 中用于训练模型的核心方法。 其中里面的callbacks参数是实现模型保存、监控、以及和tensorboard联动的重要API 1 model.fit() 方法的参数及使用 必需参数 x: 训练数据的输入。可以是 NumPy 数组、TensorFlow tf.data.Dataset、Python 生…

pytorch图神经网络处理图结构数据

人工智能例子汇总:AI常见的算法和例子-CSDN博客 图神经网络(Graph Neural Networks,GNNs)是一类能够处理图结构数据的深度学习模型。图结构数据由节点(vertices)和边(edges)组成,其中节点表示实体,边表示实体之间的关系或连接。GNNs 通过在图的结构上进行信息传递和…

100.1 AI量化面试题:解释夏普比率(Sharpe Ratio)的计算方法及其在投资组合管理中的应用,并说明其局限性

目录 0. 承前1. 夏普比率的基本概念1.1 定义与计算方法1.2 实际计算示例 2. 在投资组合管理中的应用2.1 投资组合选择2.2 投资组合优化 3. 夏普比率的局限性3.1 统计假设的限制3.2 实践中的问题 4. 改进方案4.1 替代指标4.2 实践建议 5. 回答话术 0. 承前 如果想更加全面清晰地…

hexo部署到github page时,hexo d后page里面绑定的个人域名消失的问题

Hexo 部署博客到 GitHub page 后,可以在 setting 中的 page 中绑定自己的域名,但是我发现更新博客后绑定的域名消失,恢复原始的 githubio 的域名。 后面搜索发现需要在 repo 里面添加 CNAME 文件,内容为 page 里面绑定的域名&…

嵌入式C语言:大小端详解

目录 一、大小端的概念 1.1. 大端序(Big-endian) 1.2. 小端序(Little-endian) 二、大小端与硬件体系的关系 2.1. 大小端与处理器架构 2.2. 大小端与网络协议 2.3. 大小端对硬件设计的影响 三、判断系统的大小端方式 3.1.…

[LeetCode]day9 203.移除链表元素

203. 移除链表元素 - 力扣(LeetCode) 题目描述 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val val 的节点,并返回 新的头节点 。 示例 1: 输入:head [1,2,6,3,4,5,6], v…

DBUtils中QueryRunner(空参,传数据源)构造方法的区别及应用场景

关于学习Spring框架时重构DAO层时,遇到的QueryRunner构造方法的问题,回忆MySQL中DBUtils部分 1. 空参构造方法 new QueryRunner() 特点: 不绑定数据源:QueryRunner 实例内部没有 DataSource,因此无法自动获取连接。 …