Skip to content

Commit 3faf2bf

Browse files
committed
docs: 更新文档
1 parent 7359b1c commit 3faf2bf

7 files changed

Lines changed: 97 additions & 6 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
- [树和二叉树](docs/树和二叉树.md)
3535
- [](docs/堆.md)
3636
- [B+树](docs/B+树.md)
37+
- [LSM 树](docs/LSM树.md)
3738
- [字典树](docs/字典树.md)
38-
- [](docs/graph.md)
39+
- [](docs/.md)
3940
- [算法代码模板](docs/algorithm-template.md)
4041

4142
## 💻 刷题

assets/算法.xmind

76.2 KB
Binary file not shown.

docs/LSM树.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# LSM 树
2+
3+
## 什么是 LSM 树
4+
5+
LSM 树具有以下 3 个特点:
6+
7+
1. 将索引分为内存和磁盘两部分,并在内存达到阈值时启动树合并(Merge Trees);
8+
2. 用批量写入代替随机写入,并且用预写日志 WAL 技术(Write AheadLog,预写日志技术)保证内存数据,在系统崩溃后可以被恢复;
9+
3. 数据采取类似日志追加写的方式写入(Log Structured)磁盘,以顺序写的方式提高写
10+
入效率。
11+
12+
LSM 树的这些特点,使得它相对于 B+ 树,在写入性能上有大幅提升。所以,许多 NoSQL 系统都使用 LSM 树作为检索引擎,而且还对 LSM 树进行了优化以提升检索性能。
13+
14+
LSM 树就是根据这个思路设计了这样一个机制:当数据写入时,延迟写磁盘,将数据先存放在内存中的树里,进行常规的存储和查询。当内存中的树持续变大达到阈值时,再批量地以块为单位写入磁盘的树中。因此,LSM 树至少需要由两棵树组成,一棵是存储在内存中较小的 C0 树,另一棵是存储在磁盘中较大的 C1 树。
15+
16+
### 如何将内存数据与磁盘数据合并
17+
18+
可以参考两个有序链表归并排序的过程,将 C0 树和 C1 树的所有叶子节点中存储的数据,看作是两个有序链表,那滚动合并问题就变成了我们熟悉的两个有序链表的归并问题。不过由于涉及磁盘操作,那为了提高写入效率和检索效率,我们还需要针对磁盘的特性,在一些归并细节上进行优化。
19+
20+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220316105440.png)
21+
22+
由于磁盘具有顺序读写效率高的特性,因此,为了提高 C1 树中节点的读写性能,除了根节点以外的节点都要尽可能地存放到连续的块中,让它们能作为一个整体单位来读写。这种包含多个节点的块就叫作多页块(Multi-Pages Block)。
23+
24+
第一步,以多页块为单位,将 C1 树的当前叶子节点从前往后读入内存。读入内存的多页块,叫作清空块(Emptying Block),意思是处理完以后会被清空。
25+
26+
第二步,将 C0 树的叶子节点和清空块中的数据进行归并排序,把归并的结果写入内存的一个新块中,叫作填充块(Filling Block)。
27+
28+
第三步,如果填充块写满了,我们就要将填充块作为新的叶节点集合顺序写入磁盘。这个时候,如果 C0 树的叶子节点和清空块都没有遍历完,我们就继续遍历归并,将数据写入新的填充块。如果清空块遍历完了,我们就去 C1 树中顺序读取新的多页块,加载到清空块中。
29+
30+
第四步,重复第三步,直到遍历完 C0 树和 C1 树的所有叶子节点,并将所有的归并结果写入到磁盘。这个时候,我们就可以同时删除 C0 树和 C1 树中被处理过的叶子节点。这样就完成了滚动归并的过程。
31+
32+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220316110736.png)
33+
34+
### LSM 树是如何检索
35+
36+
因为同时存在 C0 和 C1 树,所以要查询一个 key 时,我们会先到 C0 树中查询。如果查询到了则直接返回;如过没有查询到,则查询 C1 树。
37+
38+
需要注意一种特殊情况:删除操作。假设某数据在 C0 树中被删除了,但是在 C1 树中仍存在。这此时查询时,可以在 C1 树中查到这个 key,这其实是过期数据了,如何应对这种情况呢?对于被删除的数据,可以将这些数据的 key 插入到 C0 树中,并标记一个删除标志。如果查到了一个带着删除标志的 key,就直接返回查询失败。
39+
40+
## 为什么需要 LSM 树
41+
42+
在关系型数据库中,通常使用 B+ 树作为索引。B+ 树的数据都存储在叶子节点中,而叶子节点一般都存储在磁盘中。因此,每次插入的新数据都需要随机写入磁盘,而随机写入的性能非常慢。如果是一个日志系统,每秒钟要写入上千条甚至上万条数据,这样的磁盘操作代价会使得系统性能急剧下降,甚至无法使用。
43+
44+
操作系统对磁盘的读写是以块为单位的,我们能否以块为单位写入,而不是每次插入一个数据都要随机写入磁盘呢?这样是不是就可以大幅度减少写入操作了呢?解决方案就是:**LSM 树**(Log Structured Merge Trees)。
45+
46+
## WAL 技术
47+
48+
LSM 树至少需要由两棵树组成,一棵是存储在内存中较小的 C0 树,另一棵是存储在磁盘中较大的 C1 树。
49+
50+
如果机器断电或系统崩溃了,那内存中还未写入磁盘的数据岂不就永远丢失了?这种情况我们该如何解决呢?
51+
52+
为了保证内存中的数据在系统崩溃后能恢复,可以使用 WAL 技术(Write Ahead Log,预写日志技术)将数据第一时间高效写入磁盘进行备份。
53+
54+
WAL 技术保存和恢复数据的具体步骤如下:
55+
56+
1. 内存中的程序在处理数据时,会先将对数据的修改作为一条记录,顺序写入磁盘的 log 文件作为备份。由于磁盘文件的顺序追加写入效率很高,因此许多应用场景都可以接受这种备份处理。
57+
2. 在数据写入 log 文件后,备份就成功了。接下来,该数据就可以长期驻留在内存中了。
58+
3. 系统会周期性地检查内存中的数据是否都被处理完了(比如,被删除或者写入磁盘),并且生成对应的检查点(Check Point)记录在磁盘中。然后,我们就可以随时删除被处理完的数据了。这样一来,log 文件就不会无限增长了。
59+
4. 系统崩溃重启,我们只需要从磁盘中读取检查点,就能知道最后一次成功处理的数据在 log 文件中的位置。接下来,我们就可以把这个位置之后未被处理的数据,从 log 文件中读出,然后重新加载到内存中。
60+
61+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220316104837.png)
62+
63+
## 参考资料
64+
65+
- [检索技术核心 20 讲](https://time.geekbang.org/column/intro/100048401)

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
- [树和二叉树](树和二叉树.md)
2424
- [](堆.md)
2525
- [B+树](B+树.md)
26+
- [LSM 树](LSM树.md)
2627
- [字典树](字典树.md)
27-
- [](graph.md)
28+
- [](.md)
2829
- [算法代码模板](algorithm-template.md)
2930

3031
## 💻 刷题

docs/sidebar.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
- [树和二叉树](树和二叉树.md)
1111
- [](堆.md)
1212
- [B+树](B+树.md)
13-
- [字典树](trie.md)
14-
- [](graph.md)
13+
- [LSM 树](LSM树.md)
14+
- [字典树](字典树.md)
15+
- [](图.md)

docs/graph.md renamed to docs/图.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/data-structure/graph/graph.png)
66

7-
## 术语
7+
## 什么是图
88

99
- **阶(Order)** - 图 G 中点集 V 的大小称作图 G 的阶。
1010
- **子图(Sub-Graph)** - 当图 G'=(V',E')其中 V‘包含于 V,E’包含于 E,则 G'称作图 G=(V,E)的子图。每个图都是本身的子图。
@@ -20,6 +20,10 @@
2020
- **轨迹(Track)** - 如果路径 P(u,v)中的顶点各不相同,则该路径称为 u 到 v 的一条轨迹。闭的轨迹称作圈(Cycle)。
2121
- **桥(Bridge)** - 若去掉一条边,便会使得整个图不连通,该边称为[](https://baike.baidu.com/item/%E6%A1%A5)
2222

23+
如果图的边没有方向性,则被成为无向图。
24+
25+
![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20220314093554.jpg)
26+
2327
## 图的基本操作
2428

2529
- 创建一个图结构 - CreateGraph(G)
@@ -33,3 +37,7 @@
3337
- 插入一条边 - InsertEdge(G,v,w)
3438
- 删除一条边 - DeleteEdge(G,v,w)
3539
- 遍历图 - Traverse(G,v)
40+
41+
## 参考资料
42+
43+
- [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301)

docs/数组和链表.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
- 链表用 **不连续** 的内存空间来存储数据;并通过一个指针按顺序将这些空间串起来,形成一条链。
2727
- 特性差异:
2828
- 数组的 **查找** 效率高于链表。
29-
- 链表的 **增删节点** 效率高于数组。
29+
- 链表的 **添加****删除** 效率高于数组。
3030

3131
### 1.1. 数组的特性
3232

@@ -329,6 +329,21 @@ public DListNode<E> find(E value) {
329329
}
330330
```
331331

332+
## 数组和链表的比较
333+
334+
- **存储方式**
335+
- 数组用 **连续** 的内存空间来存储数据。
336+
- 链表用 **不连续** 的内存空间来存储数据;并通过一个指针按顺序将这些空间串起来,形成一条链。
337+
- **访问方式**
338+
- 数组**支持随机访问**。根据下标随机访问的时间复杂度为 `O(1)`
339+
- 链表**不支持随机访问**,只能顺序访问。
340+
- **空间大小**
341+
- 数组空间**大小固定**,扩容只能采用复制数组的方式。
342+
- 链表空间**大小不固定**,扩容灵活。
343+
- **效率比较**
344+
- 数组的 **查找** 效率高于链表。
345+
- 链表的 **添加****删除** 效率高于数组。
346+
332347
## 3. 参考资料
333348

334349
- https://zh.wikipedia.org/wiki/数组

0 commit comments

Comments
 (0)