当我们学习存储算法和索引算法时,他们可以深入了解如何在系统中存储和查询数据。因为存储和查询数据是许多系统的核心功能之一,例如数据库、搜索引擎等。理解这些算法可以帮助程序员更好地设计和优化系统架构,提高系统的可扩展性、可用性和性能。
例如,在一个需要高效查询数据的系统中,程序员可以使用B+树或哈希表等高效的索引算法来存储和查询数据。这可以大大提高查询性能,让用户能够更快地获取他们需要的数据。而在一个需要处理和分析大量文本数据的搜索引擎系统中,程序员可以使用倒排索引等高效的存储算法来存储文本数据,并使用TF-IDF、BM25等搜索算法来进行文本匹配和排序。这可以帮助搜索引擎更好地满足用户的搜索需求,提高用户的搜索体验。
总之,学习存储算法和索引算法可以帮助程序员更好地理解系统架构的核心功能和设计原理,从而设计和优化系统架构,提高系统的性能和可扩展性。
前言
**日志(Log)**在计算机领域是一种记录信息的方法。可以把它想象成一本日记,计算机系统、程序或应用在运行过程中,把它们所发生的事情都记录下来。这些记录可以包括操作、事件、错误信息等,就像我们在日记里记录生活中发生的点滴一样。
我们通常理解的日志的作用主要有以下几点:
-
跟踪与监控:日志能帮助我们了解程序的运行状态,从而可以监控系统的性能和稳定性。通过查看日志,我们可以知道程序是否按照预期执行,是否出现了异常情况。
-
问题排查:当程序出现问题时,日志是排查问题的重要线索。通过分析日志中的错误信息或异常行为,我们可以找出问题的原因,从而进行修复和优化。
-
安全审计:日志还可以用于安全审计。通过记录用户操作和系统事件,可以帮助我们分析潜在的安全风险,从而保护系统免受恶意攻击。
-
法规遵从:对于一些特定行业或应用,根据法规要求,需要对操作过程进行记录。这时,日志就可以起到证明合规性的作用。
**如果在更普遍的场景下使用日志这一词:表示一个仅追加的记录序列。**它可能压根就不是给人类看的,使用二进制格式,并仅能由其他程序读取。日志就像是计算机世界里的“黑匣子”,它记录了程序运行过程中的重要信息,有助于我们了解程序状态、排查问题、保证安全和遵从法规。
日志的最初阶段其实就是一个文本文件,每行包含一条逗号分隔的键值对(忽略转义问题的话,大致与CSV文件类似)。每次对添加数据的调用都会向文件末尾追加记录,所以更新键的时候旧版本的值不会被覆盖 —— 因而查找最新值的时候,需要找到文件中键最后一次出现的位置。我们所有的索引本质上最终都是为了完成对与日志数据的搜索,所以了解日志还是很有必要的
索引背后的思想是,保存一些额外的元数据作为路标,帮助你找到想要的数据。如果想在同一份数据中以几种不同的方式进行搜索,那么也许需要不同的索引,建在数据的不同部分上。而索引就是典型的空间换时间的思想,维护额外的结构会产生开销,特别是在写入时。
下面我们就来详细说下各种不同的索引,以及其解决了什么问题
哈希索引
首先我们需要思考一个问题,日志随着系统的运行,其占用磁盘空间的大小在不断变大,如何避免最终用完磁盘空间?
一种好的解决方案是,将日志分为特定大小的段,当日志增长到特定尺寸时关闭当前段文件,并开始写入个新的段文件。然后,我们就可以对这些段进行压缩。压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。 每个段现在都有自己的内存散列表,将键映射到文件偏移量。第一个段找不到就找第二个,以此类推 。
但也需要考虑如下问题:
- 文件格式:用二进制格式更快,更简单,首先以字节为单位对字符串的长度进行编码,然后使用原始字符串
- 删除记录:当日志段被合并时,逻辑删除告诉合并过程放弃删除键的任何以前的值。
- 崩溃恢复:崩溃重启时索引建立耗时长 部分写入记录:
- 数据完整性校验
- 并发控制:简单的实现选择是只有一个写入器线程。数据文件段是附加的,否则是不可变的,所以它们可以被多个线程同时读取。
哈希索引(Hash Index)是一种基于哈希表(Hash Table)的索引结构,它的主要设计思想是将数据的键(Key)通过哈希函数(Hash Function)映射到哈希表的一个位置,然后将数据的值(Value)存储在这个位置。哈希索引的优势在于查询速度快,时间复杂度接近O(1)。
在Redis中,哈希索引被用于存储和检索键值对,确保查询性能非常高:
- Redis使用哈希表来存储键值对。当一个新的键值对需要存储到Redis中时,Redis首先使用哈希函数将键映射到哈希表的某个位置。例如,它可以使用MurmurHash2哈希函数进行计算。
- 在Redis哈希表中,每个位置(也称为槽,Slot)都可以存储一个或多个键值对。为了处理哈希冲突,Redis采用了链地址法,即在每个槽中维护一个链表。当多个键映射到同一个槽时,它们会被存储在这个槽对应的链表中。
- 当用户查询某个键对应的值时,Redis首先使用哈希函数计算这个键对应的槽。然后在这个槽对应的链表中查找该键,找到后返回其对应的值。如果链表较短,查询速度会非常快。
- 为了保持高性能,Redis会在哈希表的大小和已使用的槽数量达到一定比例时进行自动扩容。扩容过程中,Redis会重新计算所有键对应的槽,并重新分配键值对。
通过这种方式,Redis充分利用了哈希索引的优势,实现了高性能的键值存储和查询。这也是Redis成为许多应用和服务(如缓存、消息队列、实时分析等)的理想选择的原因之一。
哈希表索引也有局限性:
- 随机IO导致要放到内存
- 范围查询不行
- hash冲突问题
SSTables和LSM树
进一步的,如果我们对段文件的格式做一个简单的改变:我们要求键值对的序列按键排序。这个格式称为排序字符串表(Sorted String Table),简称SSTable。我们还要求每个键只在每个合并的段文件中出现一次(压缩过程已经保证)。与使用散列索引的日志段相比,SSTable有几个很大的优势:
- 合并段是简单而高效的,即使文件大于可用内存。就像归并排序算法
当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。
2. 为了在文件中找到一个特定的键,不再需要保存内存中所有键的索引。类似于跳表思想
那么,我们应该如何维护这种结构:
- 写入时,将其添加到内存中的平衡树数据结构(例如,红黑树)。这个内存树有时被称为内存表(memtable)。
- 当内存表大于某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。这可以高效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新部分。当SSTable被写入磁盘时,写入可以继续到一个新的内存表实例。
- 为了提供读取请求,首先尝试在内存表中找到关键字,然后在最近的磁盘段中,然后在下一个较旧的段中找到该关键字。
- 有时会在后台运行合并和压缩过程以组合段文件并丢弃覆盖或删除的值。
因为这种数据是放在内存的,所以存在数据丢失的问题,解决数据丢失问题:可以在磁盘上保存一个单独的日志,每个写入都会立即被附加到磁盘上,SSTable写出后就可以删除
基于这种合并和压缩排序文件原理的存储引擎通常被称为LSM存储引擎。
LSM(Log-Structured Merge Tree)存储引擎是一种用于实现键值存储或文档存储的数据库存储引擎,它的特点是支持高性能写入和读取、快速的范围查询和支持高度可扩展性。常见的应用场景包括:
-
分布式数据库:在分布式数据库中,LSM存储引擎可用于实现分布式键值存储或文档存储。它可以通过数据分片和数据副本等方式来提高数据可用性和容错性,并支持在多个节点上进行并行查询和写入操作。
-
日志存储:LSM存储引擎可以用于存储大规模的日志数据,如应用程序日志、访问日志等。由于LSM存储引擎的写入性能非常高,可以支持高并发的写入操作,并且支持压缩和归档等功能,因此在日志存储方面有很好的应用前景。
-
高性能缓存:由于LSM存储引擎支持高性能写入和读取,因此可以用于实现高性能缓存,如分布式缓存、本地缓存等。它可以通过缓存热点数据和支持自动过期等功能,提高应用程序的响应速度和并发能力。
-
分布式文件系统:LSM存储引擎还可以用于实现分布式文件系统。在分布式文件系统中,LSM存储引擎可以作为元数据存储层,用于存储文件和目录的元数据信息,如文件名、大小、创建时间等,从而提高文件系统的性能和可扩展性。
进一步优化:
1、解决数据击穿问题:布隆过滤器
B树
像SSTables一样,B树保持按键排序的键值对,这允许它支持高效的键值查找和范围查询。
前面看到的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序编写段。相比之下,B树将数据库分解成固定大小的块或页面,传统上大小为4KB(有时会更大),并且一次只能读取或写入一个页面。这种设计更接近于底层硬件,因为磁盘也被安排在固定大小的块中。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在磁盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,这就形成了B树组织形式的基础。
B树是使用覆盖模式,B树的基本底层写操作是用新数据覆盖磁盘上的页面。假定覆盖不改变页面的位置;当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(如LSM树)形成鲜明对比,后者只附加到文件(并最终删除过时的文件),但从不修改文件。(但我们需要考虑到,如果写入的数据比原有的数据要多,原来位置的空闲空间已经不能满足,应该怎么办呢)
由于将数据库分解为固定大小的块或页面,我们写入数据时,也就可能存在跨页面写,进而引发以下问题:
- 读取时的不一致性:如果写入数据跨越了多个页面或块,而写操作在中间中断或出现故障,则可能会导致某些页面或块只写入了部分数据,而其他页面或块则没有写入数据。在读取这些数据时,可能会导致数据不完整或不一致的问题。
- 写入时的不一致性:如果多个并发写操作同时跨越了同一个页面或块,则可能会出现写入冲突,导致数据不一致的问题。例如,一个写操作写入了数据的前半部分,而另一个写操作写入了数据的后半部分,则最终的数据可能是不完整或不正确的。
- 性能问题:跨越多个页面或块进行写操作,会增加磁盘IO的次数,降低系统的写入性能。
为了解决这些问题,通常采用的方式是使用预写式日志(WAL)技术,将数据先写入到日志文件中,再写入到磁盘上,从而避免了跨多个页面写入可能导致的数据不一致问题。
预写式日志(WAL, write-ahead-log)(也称为重做日志(redo log)),解决写入时由于可能跨多个页面可能导致的数据不一致,其主要思想可以概括为以下几点:
- 数据先写入日志,再写入磁盘:在进行数据写入操作时,WAL先将数据写入到日志文件中,然后再将数据写入到实际的数据文件中。这种方式可以保证在出现故障或异常情况时,系统可以从日志文件中恢复数据,从而避免了数据丢失或不一致的问题。
- 日志顺序写入,避免跨多个页面写入:WAL将所有的数据都写入到一个顺序的日志文件中,避免了跨多个页面写入,从而避免了可能出现的数据不一致问题。因为日志文件中的数据是按照写入的顺序排列的,所以不会出现多个页面之间的交错写入,从而保证了数据的一致性。
- 定期刷写到磁盘上:WAL在将数据写入到日志文件后,并不是立即将数据写入到磁盘上,而是定期进行刷写。这样做可以减少磁盘的IO操作次数,提高系统的性能。
B树优化:
- 一些数据库(如LMDB)使用写时复制方案
- 我们可以通过不存储整个键来节省页面空间,但可以缩小它的大小。
- 实现尝试布局树,使得叶子页面按顺序出现在磁盘上。但是,随着树的增长,维持这个顺序是很困难的。(解决随机IO问题)
- 额外的指针已添加到树中。例如,每个叶子页面可以在左边和右边具有对其兄弟页面的引用,这允许不跳回父页面就能顺序扫描。
比较B树和LSM树
常LSM树的写入速度更快,而B树的读取速度更快。LSM树上的读取通常比较慢,因为它们必须在压缩的不同阶段检查几个不同的数据结构和SSTables
- LSM树通常能够比B树支持更高的写入吞吐量,顺序写
- LSM树可以被压缩得更好,因此经常比B树在磁盘上产生更小的文件。B树存储引擎会产生内存碎片。LSM树定期重写SSTables以去除碎片
- LSM树的压缩操作会影响并发访问。随着数据的增加,压缩的时间也会增加,所需的磁盘带宽就越多
- B树的一个优点是每个键只存在于索引中的一个位置,而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。方便使用锁实现事务
索引的存储形式
索引中的关键字是查询搜索的内容,但是该值可以是以下两种情况之一:它可以是实际的数据,也可以是对存储在别处的行的引用。在后一种情况下,行被存储的地方被称为堆文件(heap file),并且存储的数据没有特定的顺序(它可以是仅附加的,或者可以跟踪被删除的行以便用新数据覆盖它们后来)。堆文件方法很常见,因为它避免了在存在多个二级索引时复制数据:每个索引只引用堆文件中的一个位置,实际的数据保存在一个地方。 在不更改键的情况下更新值时,堆文件方法可以非常高效:只要新值不大于旧值,就可以覆盖该记录。如果新值更大,情况会更复杂,因为它可能需要移到堆中有足够空间的新位置。在这种情况下,要么所有的索引都需要更新,以指向记录的新堆位置,或者在旧堆位置留下一个转发指针
从索引到堆文件的额外跳跃对读取来说性能损失太大,因此可能希望将索引行直接存储在索引中。这被称为聚集索引。
最常见的多列索引被称为连接索引
多维索引(multi-dimensional index)是一种查询多个列的更一般的方法,这对于地理空间数据尤为重要。标准的B树或者LSM树索引不能够高效地响应这种查询(一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规B树索引。更普遍的是,使用特殊化的空间索引,例如R树)