<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>存储引擎 on codedump notes</title>
    <link>https://www.codedump.info/zh/tags/%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E/</link>
    <description>Recent content in 存储引擎 on codedump notes</description>
    <generator>Hugo</generator>
    <language>zh</language>
    <lastBuildDate>Sun, 04 Sep 2022 09:10:31 +0800</lastBuildDate>
    <atom:link href="https://www.codedump.info/zh/tags/%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>周刊（第24期）：sqlite并发读写的演进之路</title>
      <link>https://www.codedump.info/zh/post/20220904-weekly-24/</link>
      <pubDate>Sun, 04 Sep 2022 09:10:31 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220904-weekly-24/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文梳理sqlite并发读写方案的演进之路。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;sqlite并发读写的演进之路&#34;&gt;&#xA;  sqlite并发读写的演进之路&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sqlite%e5%b9%b6%e5%8f%91%e8%af%bb%e5%86%99%e7%9a%84%e6%bc%94%e8%bf%9b%e4%b9%8b%e8%b7%af&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;概论&#34;&gt;&#xA;  概论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;sqlite底层的存储基于B-tree，B-Tree对底层存储的基本读写单位是页面，而每个页面都由全局唯一的页面编号与之对应，一般来说页面编号从1开始递增。&lt;/p&gt;&#xA;&lt;p&gt;类B-Tree的存储引擎修改数据的流程如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b-tree&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/b-tree.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b-tree &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;从上图中，需要区分B-Tree类的存储引擎几个核心的模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;B-Tree算法模块：从页面管理器中读取页面到内存，进行逻辑的修改，修改完毕之后标记该页面为脏页面，这样页面管理器就知道哪些页面被修改，后续需要进行落盘。&lt;/li&gt;&#xA;&lt;li&gt;页面管理器：负责向B-Tree算法模块提供根据页面编号读、写页面的接口。&lt;/li&gt;&#xA;&lt;li&gt;数据库文件：这其实不是一个模块，泛指在磁盘上的数据库相关文件，任何的修改最终都要落到数据库文件。在sqlite中，数据库文件是单一文件，在其他存储引擎里可能是一组相关的文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;最上层的B-Tree算法模块，在进行写事务的时候，是首先向页面管理器发起读页面到内存中的请求，注意到B-Tree模块并不会直接跟数据库文件打交道，而是经过页面管理器模块（下面会展开说），修改了页面之后标记为“脏页面”，页面管理器最终负责将脏页面落盘到数据库文件中。&lt;/p&gt;&#xA;&lt;p&gt;现在来谈谈“页面管理器”模块的具体工作，也有的实现称为“缓存管理器（buffer manager）”。这个模块负责：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在内存中管理页面，这涉及到两部分内容：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果页面当前不在内存中，需要根据页面编号到磁盘上加载页面。&lt;/li&gt;&#xA;&lt;li&gt;页面也并不是每一次读写时都要到磁盘上加载，有些时候页面已经在缓存中存在了，这种情况下不需要到磁盘上加载页面数据。于是，“页面管理器”模块还需要负责维护这些内存中的页面缓存，何时淘汰这些页面、淘汰哪些内存中的页面、何时真正从磁盘上加载，都是这个模块的工作。&lt;/li&gt;&#xA;&lt;li&gt;对外部而言（这里的外部更多的是B-Tree算法模块），其实不需要也看不到页面缓存的细节，页面管理器对外提供根据页面编号读、写页面接口即可。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;错误的恢复、事务的管理。比如：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一次事务要修改N个页面，修改到中间的时候，进程崩溃了，这时候重新启动时需要恢复到这个事务之前的数据成功启动，即需要提供回滚事务的功能。&lt;/li&gt;&#xA;&lt;li&gt;同样的一个事务要修改N个页面，在事务还未提交的时候，如果事务级别不是read uncommitted， 那么前面的修改效果不能被其他事务可见，这也是页面管理器需要做的事情，毕竟它对外提供了读、写页面的接口，同一个页面编号的页面什么时候的内容可见都由它来决定。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了这些基础的了解，我们来看看sqlite在并发读写方面的演进之路。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;journal&#34;&gt;&#xA;  Journal&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#journal&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;最早的页面管理器实现是基于Journal文件的，这个文件存储页面在修改之前的内容：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;journal&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/journal.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; journal &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到的是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Journal文件存储了一个事务所要修改的页面在修改之前的内容，这个定义有点拗口，姑且称为“旧页面内容”。&lt;/li&gt;&#xA;&lt;li&gt;每次一个事务提交之后，意味着这个事务所有队页面的修改都已经落到了数据库文件中，这时候Journal文件里保存的旧页面内容就不再需要了，可以被删除了。&lt;/li&gt;&#xA;&lt;li&gt;由于每次事务修改都要落盘到数据库文件，这些落盘操作涉及到多次磁盘寻道，即一次事务多次随机磁盘寻道，这样代价其实是很大的。&lt;/li&gt;&#xA;&lt;li&gt;当需要事务回滚的功能时，页面管理器就可以从Journal文件中读出来旧页面内容覆盖回去。&lt;/li&gt;&#xA;&lt;li&gt;虽然这个算法很简单，但是缺陷也明显：它没有任何的读写并发支持。每次开始一个写事务，从开始写事务，到这个写事务提交完成的过程中间，其他的读写事务都不能开始，可以说是“一写全卡住”。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;wal&#34;&gt;&#xA;  WAL&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;从上面的分析可以看出，以Journal文件的机制，每次写事务：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;需要把内容修改全部落盘到数据库文件才能算完成。&lt;/li&gt;&#xA;&lt;li&gt;这个过程中间，不能同时存在其他并发的读、写操作。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从sqlite3.7.0版本开始（&lt;a href=&#34;https://www.sqlite.org/releaselog/3_7_0.html&#34;&gt;SQLite Release 3.7.0 On 2010-07-21&lt;/a&gt;），sqlite引入了更常见的WAL机制来解决页面的读写并发问题，WAL的原理如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;wal&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/wal.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; wal &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;WAL机制中，事务对页面的修改：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;并没有马上落到数据库文件里，而是首先写入WAL文件中。这样有两个好处：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;WAL文件是append-only的文件，在文件结尾处添加新内容，对写磁盘文件这种操作而言是更快的，因为少了很多磁盘寻道的流程。&lt;/li&gt;&#xA;&lt;li&gt;由于事务的修改并没有马上落盘到数据库文件，所以就并不可见，后续如果需要回滚事务的修改也更容易：不要这个事务修改的那部分WAL内容即可。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;由于修改有时候还未落盘，需要维护一个wal中页面的索引，用于根据页面编号定位到WAL中的页面。由于wal索引可以控制哪些wal文件内容“可见”，于是就能控制未提交的事务修改对读操作并不可见了。&lt;/li&gt;&#xA;&lt;li&gt;WAL文件不能一直增长下去，需要定期把WAL文件中已经提交的事务修改内容落盘到数据库文件，这个流程被称为“checkpoint”。在“checkpoint”之后，wal索引就可以修改了。虽然checkpoint过程将WAL文件中的内容落盘到数据库文件，仍然是针对数据库文件的随机写流程，有很多磁盘寻道操作，但是由于一次checkpoint累计了多次写事务一次性落盘，代价小了一些。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;有了WAL之后，读写并发有了一些改善：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;虽然同一时间仍然只能有一个写事务在进行，但是读事务同时存在多个。其核心原因是因为修改并没有马上直接落盘到数据库文件中，这样修改的可见性就可以由wal索引来控制，即：写事务尽管写，读事务尽管读，只要控制这些写事务的修改不在wal索引中可见即可。&lt;/li&gt;&#xA;&lt;li&gt;WAL虽然支持“一写多读”，而不是Journal文件那样的“一写全卡住”，但是还有一个问题没有解决：在做checkpoint操作的时候，连写事务也不能进行了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;两个可能的优化方案&#34;&gt;&#xA;  两个可能的优化方案&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%a4%e4%b8%aa%e5%8f%af%e8%83%bd%e7%9a%84%e4%bc%98%e5%8c%96%e6%96%b9%e6%a1%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;以下介绍sqlite目前在讨论的两个优化方案，之所以说是“可能”，因为看这部分代码还并没有合并到主干中，目前暂时还在分支里，参见：https://github.com/sqlite/sqlite/tree/begin-concurrent-pnu-wal2。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;wal-2&#34;&gt;&#xA;  WAL-2&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal-2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;为了解决“checkpoint时无法进行写事务”的痛点，sqlite目前在尝试新的WAL-2机制。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;wal-2&#34; src=&#34;https://www.codedump.info/media/imgs/20220904-weekly-24/wal-2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; wal-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;引入WAL-2之后，同时有两个WAL文件，这样可以：checkpoint其中一个WAL文件时，继续写另一个WAL文件，下一次再进行checkpoint时进行切换，这样checkpoint就不会阻塞住写操作。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;begin-concurrent&#34;&gt;&#xA;  BEGIN CONCURRENT&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#begin-concurrent&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;目前的WAL机制，都只能支持同一时间一个写事务，&lt;code&gt;BEGIN CONCURRENT&lt;/code&gt;机制可以实现多个写并发，这篇&lt;a href=&#34;https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md&#34;&gt;SQLite: Begin Concurrent&lt;/a&gt;文档中，大概描述了一下这个优化的思路：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第23期）：图解Blink-Tree：B&#43;Tree的一种并发优化结构和算法</title>
      <link>https://www.codedump.info/zh/post/20220807-weekly-23/</link>
      <pubDate>Sun, 07 Aug 2022 10:33:38 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220807-weekly-23/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：&lt;a href=&#34;https://www.csd.uoc.gr/~hy460/pdf/p650-lehman.pdf&#34;&gt;《Efficient Locking for Concurrent Operations on B-Trees 》&lt;/a&gt;论文中提出了一种称为“Blink-Tree”的数据结构，这个数据结构提供了B+Tree并发访问的一些优化方式，本文对这篇论文进行解读。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概论&#34;&gt;&#xA;  概论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;由于Blink-Tree本质上是B+Tree的一种优化，所以要理解它首先要对B+Tree有一些了解，在这以前介绍过B+Tree，就不在这里阐述了，可以参考：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我们来看如果同时存在两个读写操作并发访问一颗B+Tree，会出现什么问题，见下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b&amp;#43;tree-1&#34; src=&#34;https://www.codedump.info/media/imgs/20220807-weekly-23/b&amp;#43;tree-1.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b+tree-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;进程P1查询数据15，而进程P2写入数据9，当P2写入数据完毕时，树结构变成了下图这样：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b&amp;#43;tree-2&#34; src=&#34;https://www.codedump.info/media/imgs/20220807-weekly-23/b&amp;#43;tree-2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b+tree-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于原先的叶子节点要满足B+Tree的性质，所以分成了两个叶子节点，而这时P1进程对此并没有感知，还停留在旧的节点上，于是就导致了查询数据15失败。&lt;/p&gt;&#xA;&lt;p&gt;一种最直观的优化方式是读、写的时候加全局锁，但是这样做的效率不高。Blink-Tree就是为了高效解决这类并发访问问题引入的一种结构和算法。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据结构&#34;&gt;&#xA;  数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Blink-Tree本质上还是一颗B+Tree，即数据存储在叶子节点上的B-Tree。&lt;/p&gt;&#xA;&lt;p&gt;对于一颗&lt;code&gt;k-degree&lt;/code&gt;的Blink-Tree而言，它有如下的性质：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;所有叶子节点是同一高度的，即从根节点到每个叶子节点都是同一长度。（Each path from the root to any leaf has the same length, h.）&lt;/li&gt;&#xA;&lt;li&gt;对于每个内部节点而言，除非是根节点，否则都至少有&lt;code&gt;k+1&lt;/code&gt;子节点。（Each node except the root and the leaves has at least k + 1 sons.）&lt;/li&gt;&#xA;&lt;li&gt;根节点要么是叶子节点，否则至少有两个子节点。（The root is a leaf or has at least two sons.）&lt;/li&gt;&#xA;&lt;li&gt;内部节点最多有&lt;code&gt;2k+1&lt;/code&gt;个子节点（Each node has at most 2k + 1 sons），结合上面的内容即内部节点的子节点数量在&lt;code&gt;[k+1,2k+1]&lt;/code&gt;之间。&lt;/li&gt;&#xA;&lt;li&gt;数据都存储在叶子节点上。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，上面的性质和B+Tree很相似，在此基础上Blink-Tree还增加了以下数据：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第16期）：图解ARIES论文（下）</title>
      <link>https://www.codedump.info/zh/post/20220521-weekly-16/</link>
      <pubDate>Sat, 21 May 2022 11:46:44 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220521-weekly-16/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：ARIES(Algorithm for Recovery and Isolation Exploiting Semantics的简称）是论文&lt;a href=&#34;https://cs.stanford.edu/people/chrismre/cs345/rl/aries.pdf&#34;&gt;《ARIES: A Transaction Recovery Method Supporting Fine-Franularity Locking and Partial Rollbacks Using Write-Ahead Logging》&lt;/a&gt;中提到的一种存储引擎中数据恢复的算法。这篇论文可以说是存储引擎数据恢复领域必读的一篇论文，这两期的周刊就是对这篇论文的图解，这是其中的下篇。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;图解aries论文下&#34;&gt;&#xA;  图解ARIES论文（下）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%be%e8%a7%a3aries%e8%ae%ba%e6%96%87%e4%b8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;前情回顾&#34;&gt;&#xA;  前情回顾&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%89%8d%e6%83%85%e5%9b%9e%e9%a1%be&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20220514-weekly-15/&#34;&gt;周刊（第15期）：图解ARIES论文（上）&lt;/a&gt;中，讨论了存储引擎面临的问题，如果存储引擎宕机重启，将要进行以下两个操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;撤销（Undo）：未完成或者由于各种原因发生回滚（rollback）、中断（abort）的事务，其修改需要被撤销，即回滚为事务之前的旧值。&lt;/li&gt;&#xA;&lt;li&gt;重做（Redo）：已经提交的事务，其修改操作的效果需要体现为新值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;为了这两个操作，存储引擎就需要回答这两个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“是否允许未提交事务的修改在持久化存储上生效”（Whether the DBMS allows an uncommitted txn to overwrite the most recent committed value of an object in non-volatile storage），被称为&lt;code&gt;Steal policy&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;一个事务在提交之前是否需要将所有修改同步到持久化存储上（Whether the DBMS requires that all updates made by a txn are reflected on non-volatile storage before the txn is allowed to commit.），称为&lt;code&gt;force policy&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;两个问题合并起来一共有四种组合：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第15期）：图解ARIES论文（上）</title>
      <link>https://www.codedump.info/zh/post/20220514-weekly-15/</link>
      <pubDate>Sat, 14 May 2022 06:33:26 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220514-weekly-15/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：ARIES(Algorithm for Recovery and Isolation Exploiting Semantics的简称）是论文&lt;a href=&#34;https://cs.stanford.edu/people/chrismre/cs345/rl/aries.pdf&#34;&gt;《ARIES: A Transaction Recovery Method Supporting Fine-Franularity Locking and Partial Rollbacks Using Write-Ahead Logging》&lt;/a&gt;中提到的一种存储引擎中数据恢复的算法。这篇论文可以说是存储引擎数据恢复领域必读的一篇论文，这两期的周刊就是对这篇论文的图解，这是其中的上篇。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;图解aries论文上&#34;&gt;&#xA;  图解ARIES论文（上）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%be%e8%a7%a3aries%e8%ae%ba%e6%96%87%e4%b8%8a&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在展开解释ARIES算法原理之前，需要对&lt;a href=&#34;https://www.codedump.info/post/20220410-weekly-12/&#34;&gt;Page oriented类存储引擎&lt;/a&gt;的日志系统有一定的了解，才能继续解释基于这个日志系统之上做的恢复算法。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;问题&#34;&gt;&#xA;  问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在一个存储系统中，出错是非常常见的情况的，这就涉及到出错了之后系统恢复时还需要能继续工作，即数据不能发生破坏导致整个系统跑不起来。&lt;/p&gt;&#xA;&lt;p&gt;于是，当系统出错需要重启恢复时，就涉及到以下两个动作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;撤销（Undo）：未完成或者由于各种原因发生回滚（rollback）、中断（abort）的事务，其修改需要被撤销，即回滚为事务之前的旧值。&lt;/li&gt;&#xA;&lt;li&gt;重做（Redo）：已经提交的事务，其修改操作的效果需要体现为新值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;来看下图中提出的问题：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;bufferpool&#34; src=&#34;https://www.codedump.info/media/imgs/20220514-weekly-15/bufferpool.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; bufferpool &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存在事务T1和T2在同时执行：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;事务T1：修改A值为3，但是在事务还未提交前，事务T2开始执行。&lt;/li&gt;&#xA;&lt;li&gt;事务T2：修改B值为8，并且成功提交。&lt;/li&gt;&#xA;&lt;li&gt;事务T1终止：在事务T2成功提交之后，事务T1终止。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个事务调度的执行顺序引发了以下几个问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;回滚未提交的事务T1需要做什么？&lt;/li&gt;&#xA;&lt;li&gt;对于未提交的事务T1，是否允许其修改操作在持久化存储上生效（即将A修改为3）？&lt;/li&gt;&#xA;&lt;li&gt;在磁盘的数据库文件中，已成功提交的事务T2，其修改操作是否应该立即落盘（即从buffer pool中同步修改的内容到硬盘）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;第一个问题当前暂且放到一边，来看后面两个问题。&lt;/p&gt;&#xA;&lt;p&gt;“是否允许未提交事务的修改在持久化存储上生效”（Whether the DBMS allows an uncommitted txn to overwrite the most recent committed value of an object in non-volatile storage），被称为&lt;code&gt;Steal policy&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;steal：允许未提交事务的修改持久化存储上生效。&lt;/li&gt;&#xA;&lt;li&gt;no steal：反之。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一个事务在提交之前是否需要将所有修改同步到持久化存储上（Whether the DBMS requires that all updates made by a txn are reflected on non-volatile storage before the txn is allowed to commit.），也有两种策略：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第12期）：Page oriented类存储引擎里可能同时存在多种结构</title>
      <link>https://www.codedump.info/zh/post/20220410-weekly-12/</link>
      <pubDate>Sun, 10 Apr 2022 11:38:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220410-weekly-12/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本期聊一聊Page oriented类存储引擎内的数据结构组织。在满足“向磁盘读写的基本单位是物理页面”这个大前提下，这类存储引擎的可能同时存在多种结构。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;page-oriented类存储引擎里可能同时存在多种树形结构&#34;&gt;&#xA;  page oriented类存储引擎里可能同时存在多种树形结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#page-oriented%e7%b1%bb%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e9%87%8c%e5%8f%af%e8%83%bd%e5%90%8c%e6%97%b6%e5%ad%98%e5%9c%a8%e5%a4%9a%e7%a7%8d%e6%a0%91%e5%bd%a2%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;存储引擎的分类&#34;&gt;&#xA;  存储引擎的分类&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e7%9a%84%e5%88%86%e7%b1%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;目前接触到的存储引擎，以向磁盘读写方式来分类的话，大体可以分为两类：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;LSM-Tree结构。&lt;/li&gt;&#xA;&lt;li&gt;Page oriented类。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;LSM-Tree是“Log-Structured Merge-Tree”的简称，这类存储引擎写入一条数据的流程大体如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;向内存以及WAL日志中写入完成，即可认为写入成功。&lt;/li&gt;&#xA;&lt;li&gt;内存中的数据写满之后，将落盘到所谓的sstable中。&lt;/li&gt;&#xA;&lt;li&gt;sstable分为多层，随着写入进行，不同层次的sstable数据将进行合并。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;LSM&#34; src=&#34;https://www.codedump.info/media/imgs/20220410-weekly-12/LSM.jpeg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; LSM &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;（图片引用自&lt;a href=&#34;https://zhuanlan.zhihu.com/p/181498475&#34;&gt;LSM树详解 - 知乎&lt;/a&gt;)&lt;/p&gt;&#xA;&lt;p&gt;从上面简单的写入LSM的流程可以看到：无论是写入内存还是磁盘，这类存储引擎在写入新数据时（不是合并sstable流程），磁盘操作的单位是一条记录。而一条记录的长度，是不定长的。&lt;/p&gt;&#xA;&lt;p&gt;与LSM-Tree类的结构不同的是，Page oriented类的存储引擎，向磁盘发起读写操作的基本单位是页面（page），一个页面通常的大小是2的次方，最小一般是1024字节，比如sqlite的存储，其页面大小为4K（可以修改编译选项配置页面大小）。&lt;/p&gt;&#xA;&lt;p&gt;以一个物理页面为读写磁盘的基本单位，这也是这一类存储引擎之所以被称为”Page oriented类存储引擎“的原因。本文重点是介绍Page oriented类存储引擎的结构。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;page-oriented存储引擎的结构&#34;&gt;&#xA;  Page oriented存储引擎的结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#page-oriented%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e7%9a%84%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;还是以之前介绍过的sqlite的架构图来开头：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree架构&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-0/btree-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree架构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这个架构由下往上依次是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;系统层：提供不同系统级API的封装，比如文件读写、加解锁操作等。&lt;/li&gt;&#xA;&lt;li&gt;物理页面管理层：提供物理页面读写、缓存等功能。&lt;/li&gt;&#xA;&lt;li&gt;树形结构的实现：根据具体的树形算法，组织物理页面之间的逻辑关系（比如父子页面、兄弟页面），以及单个物理页面之内的数据的组织。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这里的重点是页面管理层和树形结构的实现这两部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;物理页面管理相当于是磁盘文件的”原材料供应商“，负责对它的客户也就是各种不同结构的实现提供物理页面这一”原材料“的读写、缓存管理，而它对这些材料被客户拿去做成了什么，一无所知。&lt;/li&gt;&#xA;&lt;li&gt;树形结构的实现，从页面管理器拿到了”物理页面“这个原材料之后，可以按照自己的算法和数据结构任意塑造成任何合理的结构。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的物理页面组织和逻辑页面结构&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/database-file.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的物理页面组织和逻辑页面结构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;可以看到，Page oriented存储引擎，在满足“向磁盘读写的基本单位是物理页面”这个大前提下，这类存储引擎的可能同时存在多种结构：可能只有B-Tree，也可能只有B+Tree。还有另一种情况是：这类存储引擎内部同时存在多种结构。&lt;/p&gt;&#xA;&lt;p&gt;以sqlite为例，内部其实就存在两种结构：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存储索引的index tree：结构为B-Tree，键为表索引，值为这一行数据的&lt;code&gt;rowid&lt;/code&gt;，其中&lt;code&gt;rowid&lt;/code&gt;为隐藏列，创建数据表时自动生成，这一列是自增整数。&lt;/li&gt;&#xA;&lt;li&gt;存储数据的table tree：结构为B+Tree，键为&lt;code&gt;rowid&lt;/code&gt;，值为一行数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这两类存储引擎，由于同属于“Page oriented类存储引擎”，因此可以共用同一个物理页面管理器。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的rowid全量数据表和索引表&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/btree-rowid.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的rowid全量数据表和索引表 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;下面，以sqlite中的一个表为例来解释上面这个流程。&lt;/p&gt;&#xA;&lt;p&gt;首先，创建一个表以及索引：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;// &lt;span style=&#34;&#34;&gt;创建数据库&lt;/span&gt;COMPANY&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;TABLE&lt;/span&gt; COMPANY(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   ID             &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;INT&lt;/span&gt;      &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NULL&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   NAME           &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;TEXT&lt;/span&gt;    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NULL&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   AGE            &lt;span style=&#34;font-weight:bold;font-style:italic&#34;&gt;INT&lt;/span&gt;     &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;NULL&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;// &lt;span style=&#34;&#34;&gt;创建索引&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;INDEX&lt;/span&gt; id_index &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;ON&lt;/span&gt; COMPANY (id);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面这个建表以及创建索引之后，对应的在这个数据文件中就有了两个树形结构：&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第11期）：mmap适用于存储引擎吗？</title>
      <link>https://www.codedump.info/zh/post/20220327-weekly-11/</link>
      <pubDate>Sun, 27 Mar 2022 15:06:38 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220327-weekly-11/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本期聊一聊mmap技术在存储引擎中的应用。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;mmap适用于存储引擎吗&#34;&gt;&#xA;  mmap适用于存储引擎吗？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mmap%e9%80%82%e7%94%a8%e4%ba%8e%e5%ad%98%e5%82%a8%e5%bc%95%e6%93%8e%e5%90%97&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;想写这篇文章，主要源于两篇文章（论文）中的对mmap在存储引擎中使用的两种截然不同的观点讨论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;反方（mmap不应该用于存储引擎）：&lt;a href=&#34;https://db.cs.cmu.edu/mmap-cidr2022/&#34;&gt;Are You Sure You Want to Use MMAP in Your Database Management System? (CIDR 2022)&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;正方（mmap可以用于存储引擎）：&lt;a href=&#34;https://ayende.com/blog/196161-C/re-are-you-sure-you-want-to-use-mmap-in-your-database-management-system&#34;&gt;re: Are You Sure You Want to Use MMAP in Your Database Management System? - Ayende @ Rahien&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由于刚好看过这两种方式的btree存储引擎：sqlite的btree实现以及boltdb，所以可以结合我的认知来聊一聊这个问题。这两个存储引擎的实现都已经整理成了系列博客，这两个系列的第一篇分别是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200625-boltdb-1/&#34;&gt;boltdb 1.3.0实现分析（一） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;先来看看一个存储引擎实现时的大体分层，以sqlite为例分为三层：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree架构&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-0/btree-arch.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree架构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;自下而上，这三个层次分别是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;os层：封装系统级API实现文件的读写等操作。&lt;/li&gt;&#xA;&lt;li&gt;页面管理层：提供以页面为单位的读、写、加载、缓存等操作。&lt;/li&gt;&#xA;&lt;li&gt;btree实现：btree以物理页面为单位向下一层的页面管理层来读写页面，而物理页面内部的逻辑组织（比如父子关系），以及页面内的数据组织（比如一个页面中管理的数据）由这一层负责。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以这样来简单区别理解“页面管理”模块和btree模块的功能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;页面管理：顾名思义，页面管理模块的最基本单位是”页面“，页面的读、写、缓存、落盘、恢复、回滚等，都由页面模块负责。上一层依赖页面管理模块的btree模块，不需要关心一个页面何时缓存、何时落盘等细节。即：&lt;strong&gt;页面模块负责页面的物理级别的操作&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;btree：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;负责按照btree算法，来组织页面，即负责的是页面之间逻辑关系维护。&lt;/li&gt;&#xA;&lt;li&gt;除此以外，一个页面内部的数据的物理、逻辑组织，也是btree模块来负责的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;即：&lt;strong&gt;btree负责维护页面间的逻辑关系，以及一个页面内数据的组织。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;以页面物理、逻辑关系的维护看模块划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-0/page-module.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 以页面物理、逻辑关系的维护看模块划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在数据库文件中，通常按照页面为单位来划分文件，比如sqlite一般是4KB大小为一个物理页面，所以一个数据库文件可以看做是一个大的“物理页面数组”，这样的话每个物理页面都有一个对应的编号（从1开始），这个编号通常简称为PID（page id）。&lt;/p&gt;&#xA;&lt;p&gt;从上面的功能划分可以看到，“页面管理器（也被称为“buffer pool）”的功能是非常复杂的，这里列举几个最关键的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;读页面：上层的btree要读一个数据库文件中的页面时，通常传入一个PID，由页面管理器去加载这个页面的数据。而页面数据并不是每次都会到数据库文件中一次磁盘IO读出来，也很可能在内存中，此时就不需要读磁盘操作了。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;写页面：当一个页面被修改后，就被称为“脏页面（dirty page）”，需要落盘；但并不是每一次修改了一个页面的内容之后就马上落盘，其原因在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一次写事务可能修改了不止一个页面，需要以事务为单位去落盘脏页面。&lt;/li&gt;&#xA;&lt;li&gt;即便是落盘脏页面，由于涉及到写磁盘操作，所以还会用其他方式减少写磁盘的次数。比如sqlite的wal备份文件机制中，脏页面的内容是首先写入wal文件的，由于写wal文件是一次append操作而不是随机写，所以效率会更高，如果一个脏页面的内容被写入wal文件的话，那么这部分页面内容是不急于马上写入数据库文件的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;缓存页面：由于页面缓存的功能，所以还需要一个页面缓存管理的功能，主要负责：&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（五）- Btree的实现</title>
      <link>https://www.codedump.info/zh/post/20220201-sqlite-btree-5-btree/</link>
      <pubDate>Tue, 01 Feb 2022 15:55:40 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220201-sqlite-btree-5-btree/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面的内容里，详细介绍了页面管理器部分的内容，回顾一下页面管理器和Btree模块的分工：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;页面管理器：提供页面级别的物理管理，如缓存、读取、写入、页面备份等。&lt;/li&gt;&#xA;&lt;li&gt;Btree：根据btree数据结构提供页面在逻辑上的组织，以及单个页面内的划分。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;还记得最开始，研究生产级别btree实现时的几个疑问：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;数据库教科书中，演示btree算法时，使用的都是定长的简单数据。实际应用中，存储的数据都是变长的，那么应该如何存储变长的数据呢？&lt;/li&gt;&#xA;&lt;li&gt;如果一行数据的大小，超过了一个物理页面的大小，又该如何处理？&lt;/li&gt;&#xA;&lt;li&gt;删除一行数据之后，它留下的空间如何回收利用？而回收利用时，不可避免的会出现碎片的问题，比如原先10字节的数据被回收，用来存储9字节的数据，多出来的1字节数据就被浪费了，碎片问题应该如何解决？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这些问题，都与“一个物理页面内数据如何组织”这个核心问题息息相关，带着这些问题展开btree实现的讨论。&lt;/p&gt;&#xA;&lt;p&gt;在下文中，不会讨论btree算法的细节，这部分不熟悉的，可以回看之前的文章或者教科书：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;物理页面的数据组织&#34;&gt;&#xA;  物理页面的数据组织&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%89%a9%e7%90%86%e9%a1%b5%e9%9d%a2%e7%9a%84%e6%95%b0%e6%8d%ae%e7%bb%84%e7%bb%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;数据表的逻辑组织和页面类型&#34;&gt;&#xA;  数据表的逻辑组织和页面类型&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e8%a1%a8%e7%9a%84%e9%80%bb%e8%be%91%e7%bb%84%e7%bb%87%e5%92%8c%e9%a1%b5%e9%9d%a2%e7%b1%bb%e5%9e%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在展开具体的格式讨论之前，有必要先了解一下数据库文件的大体结构，已经不同的页面类型。&lt;/p&gt;&#xA;&lt;p&gt;sqlite中所谓的&lt;code&gt;数据库文件&lt;/code&gt;是单一文件，按照物理页面（2的次方）的大小来划分为多个页面。其中，每个表在数据库文件中是一棵btree的结构来组织，而不同类型的btree还区分了不同的页面。&lt;/p&gt;&#xA;&lt;p&gt;比如下图中，将平面的数据库文件，按照颜色划分成存储两个表的btree：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;数据库文件的物理页面组织和逻辑页面结构&#34; src=&#34;https://www.codedump.info/media/imgs/20220201-sqlite-btree-5-btree/database-file.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 数据库文件的物理页面组织和逻辑页面结构 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;上半部分表示，在物理的组织上，一个数据库文件以一个物理页面为基本单位来存储。&lt;/li&gt;&#xA;&lt;li&gt;下半部分表示，在逻辑的组织上，不同的表都有自己的btree树形结构，这是物理页面在逻辑上的组织方式。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为每个表都有自己的btree树形结构，如果每个表都有一个对应的根页面编号，比如图中的两个表，对应的树形结构中，根节点所在的页面分别是1和2。&lt;/p&gt;&#xA;&lt;p&gt;接着来看不同的页面类型，以及存储上的差异。&lt;/p&gt;&#xA;&lt;p&gt;以一个例子来说明，创建以下的数据库，插入数据，以及索引：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;// 创建数据库COMPANY&#xA;CREATE TABLE COMPANY(&#xA;   ID             INT      NOT NULL,&#xA;   NAME           TEXT    NOT NULL,&#xA;   AGE            INT     NOT NULL,&#xA;   ADDRESS        CHAR(50),&#xA;   SALARY         REAL&#xA;);&#xA;&#xA;// 创建索引&#xA;CREATE INDEX id_index ON COMPANY (id);&#xA;&#xA;// 插入2条数据&#xA;INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1, &amp;#39;Paul&amp;#39;, 32, &amp;#39;California&amp;#39;, 20000.00 );&#xA;INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY)&#xA;VALUES (2, &amp;#39;Allen&amp;#39;, 25, &amp;#39;Texas&amp;#39;, 15000.00 );&#xA;&#xA;// 查询数据&#xA;sqlite&amp;gt; select * from COMPANY;&#xA;1|Paul|32|California|20000.0&#xA;2|Allen|25|Texas|15000.0&#xA;&#xA;// 查询rowid和数据&#xA;sqlite&amp;gt; select rowid,* from COMPANY;&#xA;1|1|Paul|32|California|20000.0&#xA;2|2|Allen|25|Texas|15000.0&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在上面的流程里：&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（四）- WAL的实现</title>
      <link>https://www.codedump.info/zh/post/20220106-sqlite-btree-4-wal/</link>
      <pubDate>Thu, 06 Jan 2022 21:48:18 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220106-sqlite-btree-4-wal/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面两节，分别讲解了sqlite中写入事务时的并发控制框架，以及journal备份文件的实现机制。&lt;/p&gt;&#xA;&lt;p&gt;回忆一下journal备份文件的实现：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每次一个新的写事务开始之前，要首先写journal文件的文件头。&lt;/li&gt;&#xA;&lt;li&gt;写事务过程中，如果修改了哪个页面，在修改之前需要首先将这个页面的内容写入到journal文件中。&lt;/li&gt;&#xA;&lt;li&gt;写事务完成后，在同步所有缓存中被修改的页面到数据库文件之前，要首先将journal文件中的所有修改同步到磁盘，然后再修改数据库文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，journal备份的整个流程都较为原始，性能不高，所以在sqlite 3.7.0版本（&lt;a href=&#34;https://www.sqlite.org/releaselog/3_7_0.html&#34;&gt;SQLite Release 3.7.0 On 2010-07-21&lt;/a&gt;，2010-07-21）中，引入了另一种备份机制：WAL（Write Ahead Log）。&lt;/p&gt;&#xA;&lt;p&gt;本节首先介绍WAL的实现原理，然后再展开其具体的实现。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;wal工作原理&#34;&gt;&#xA;  WAL工作原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal%e5%b7%a5%e4%bd%9c%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;从前面journal的实现中可以看到，写入journal文件中的内容，是待修改页面修改之前的内容，而WAL则相反：被修改的页面内容首先写入到WAL中。&lt;/p&gt;&#xA;&lt;p&gt;用sqlite官网的文字来说，WAL文件的定义是这样的：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The write-ahead log or &amp;ldquo;wal&amp;rdquo; file is a roll-forward journal that records transactions that have been committed but not yet applied to the main database.&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（三）- journal文件备份机制</title>
      <link>https://www.codedump.info/zh/post/20211222-sqlite-btree-3-journal/</link>
      <pubDate>Wed, 22 Dec 2021 19:15:31 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211222-sqlite-btree-3-journal/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在上一节中（&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架&lt;/a&gt;），已经讲解了sqlite中的并发控制机制，里面会涉及到一个“备份页面”的模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;备份所有在一个事务中会修改到的页面。&lt;/li&gt;&#xA;&lt;li&gt;出错时回滚页面内容。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;里面也提到，有两种备份文件的机制：journal文件，以及WAL文件。今天首先讲解journal文件的实现，它的效率会更低一些，也正是因为这个原因后续推出了更优的WAL机制。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;相关命令&#34;&gt;&#xA;  相关命令&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%9b%b8%e5%85%b3%e5%91%bd%e4%bb%a4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;sqlite中，可以使用&lt;code&gt;PRAGMA journal_mode&lt;/code&gt;来修改备份文件机制，包括以下几种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;delete：默认模式。在该模式下，在事务结束时，备份文件将被删除。&lt;/li&gt;&#xA;&lt;li&gt;truncate：日志文件被截断为零字节长度。&lt;/li&gt;&#xA;&lt;li&gt;persist：日志文件被留在原地，但头部被重写，表明日志不再有效。&lt;/li&gt;&#xA;&lt;li&gt;memory：日志记录保留在内存中，而不是磁盘上。&lt;/li&gt;&#xA;&lt;li&gt;off：不保留任何备份记录。&lt;/li&gt;&#xA;&lt;li&gt;wal：采用wal形式的备份文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;其中，前面三种delete、truncate、persist都是使用journal文件来实现的备份，区别在于事务结束之后的对备份文件的处理罢了。&lt;/p&gt;&#xA;&lt;p&gt;本节首先讲解journal文件，下一节讲解wal备份文件。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;journal文件格式&#34;&gt;&#xA;  journal文件格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#journal%e6%96%87%e4%bb%b6%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;journal文件的文件名规则是：与同目录的数据库文件同名，但是多了字符串“-journal”为后缀。比如数据库文件是“test.db”，那么对应的journal文件名为“test.db-journal”。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;文件头&#34;&gt;&#xA;  文件头&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%87%e4%bb%b6%e5%a4%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;偏移量&lt;/th&gt;&#xA;          &lt;th&gt;大小&lt;/th&gt;&#xA;          &lt;th&gt;描述&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;0&lt;/td&gt;&#xA;          &lt;td&gt;8&lt;/td&gt;&#xA;          &lt;td&gt;文件头的magic number: 0xd9, 0xd5, 0x05, 0xf9, 0x20, 0xa1, 0x63, 0xd7&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;8&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;journal文件中的页面数量，如果为-1表示一直到journal文件尾&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;12&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;每次计算校验值时算出来的随机数&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;16&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;在开始备份前数据库文件的页面数量&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;20&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;磁盘扇区大小&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;24&lt;/td&gt;&#xA;          &lt;td&gt;4&lt;/td&gt;&#xA;          &lt;td&gt;journal文件中的页面大小&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这里大部分的字段都自解释了，不必多做解释，唯一需要注意的是随机数，因为这是用来后续校验备份页面的字段，这将在后面结合流程来说明。&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（二）- 并发控制框架</title>
      <link>https://www.codedump.info/zh/post/20211218-sqlite-btree-2-concurrency-control/</link>
      <pubDate>Sat, 18 Dec 2021 15:25:05 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211218-sqlite-btree-2-concurrency-control/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;按照之前起步阶段对sqlite btree整体架构的分析，“页面管理模块”分为以下几个子模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;页面缓存管理。&lt;/li&gt;&#xA;&lt;li&gt;页面备份，又分为以下两种实现：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;journal文件。&lt;/li&gt;&#xA;&lt;li&gt;WAL文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;页面管理模块。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;前面一节讲完了“页面缓存管理”的实现，按照自下往上的顺序，就应该到“页面备份”了。“页面备份”核心的工作是：在真正修改页面内容之前，将还未修改的页面内容备份，这样一旦系统在事务过程中宕机崩溃，就可以用这部分内容回滚还未落盘的事务修改，让系统回到一个正确的状态。&lt;/p&gt;&#xA;&lt;p&gt;“页面备份”有两种实现方式，在早期使用的journal文件，这种方式性能不高；在3.7版本之后，sqlite引入了WAL文件来保存页面内容，这样做的效率更高。&lt;/p&gt;&#xA;&lt;p&gt;本节就讲解这部分内容，在对这部分内容有一个总体的了解之后，继续讲解页面备份的总体流程。后面的章节再具体分析journal以及WAL的实现。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;写事务的流程&#34;&gt;&#xA;  写事务的流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%86%99%e4%ba%8b%e5%8a%a1%e7%9a%84%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;（以下流程分析，按照sqlite官网中的文档&lt;a href=&#34;https://sqlite.org/atomiccommit.html&#34;&gt;Atomic Commit In SQLite&lt;/a&gt;进行讲解，图例也全部引用自官网。）&lt;/p&gt;&#xA;&lt;p&gt;sqlite的写事务，分为以下几个流程：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;1初始化阶段initial-state&#34;&gt;&#xA;  1、初始化阶段（Initial State）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#1%e5%88%9d%e5%a7%8b%e5%8c%96%e9%98%b6%e6%ae%b5initial-state&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;初始化&#34; src=&#34;https://www.codedump.info/media/imgs/20211218-sqlite-btree-2-concurrency-control/commit-0.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 初始化 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图中，从右到左即是系统的磁盘、操作系统缓冲区、用户空间三部分，其中磁盘和操作系统缓冲区有划分为多块的空间，每一块在sqlite里被称为一个&lt;code&gt;sector&lt;/code&gt;，蓝色部分表示是修改之前的数据。&lt;/p&gt;&#xA;&lt;p&gt;这是系统初始时的样子。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;2拿到读锁acquiring-a-read-lock&#34;&gt;&#xA;  2、拿到读锁（Acquiring A Read Lock）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#2%e6%8b%bf%e5%88%b0%e8%af%bb%e9%94%81acquiring-a-read-lock&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;拿到读锁&#34; src=&#34;https://www.codedump.info/media/imgs/20211218-sqlite-btree-2-concurrency-control/commit-1.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 拿到读锁 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在开始进行写操作之前，sqlite必须先把待修改的页面加载内存中（这就是上一节“页面缓存管理器”做的事情），后续的修改其实也是首先修改这部分加载到内存中的页面内容，因为可能一次提交会修改同一个页面中的多处内容，最后才把页面内容落盘。&lt;/p&gt;&#xA;&lt;p&gt;所以，这一步所要做的，是首先拿到数据库文件的读锁（shared lock），需要说明的是，这个读锁是数据库级别的锁。同一时间，系统中可以存在多个读锁，但是只要系统中还存在读锁，就不再允许分配出新的写锁（write lock）。&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（一）- 管理页面缓存</title>
      <link>https://www.codedump.info/zh/post/20211217-sqlite-btree-1-pagecache/</link>
      <pubDate>Fri, 17 Dec 2021 14:22:06 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211217-sqlite-btree-1-pagecache/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&lt;code&gt;页面管理&lt;/code&gt;模块中，很重要的一个功能是缓存页面的内容在内存中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读页面：如果页面已经在内存，就不需要到文件中读出页面内容。&lt;/li&gt;&#xA;&lt;li&gt;写页面：如果页面已经在内存，那么对页面的修改就只需要修改页面在内存中的数据即可，被修改了但是还没有落盘的页面，被称为“脏页面（dirty page）“。这样，多次对某个页面的修改，可能最后只需要一次落盘即可。当然，对页面的修改，如果在还没有落盘之前，系统就崩溃了，这种情况下应该如何处理，这就是“崩溃恢复”模块做的事情了。本节中，将专注在“页面缓存”这个子模块的实现。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;既然要将页面缓存在内存中，就会涉及到几个功能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何知道哪些页面已经被缓存在内存里了？&lt;/li&gt;&#xA;&lt;li&gt;缓存在内存中的页面如何组织管理？&lt;/li&gt;&#xA;&lt;li&gt;缓存页面使用的内存不够用时，应该如何处理？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我们首先来了解一下“页面缓存”模块的总体划分：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;页面缓存功能的模块划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-1-pagecache/pagecache.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 页面缓存功能的模块划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;按照上图的划分，页面缓存模块分为以下几部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;页面缓存管理器：实现了页面缓存的总体算法流程，以及提供对外的接口，但是具体到“页面缓存算法”的实现，则有赖于下面这个可用户定制的&lt;code&gt;sqlite3_pcache_methods2&lt;/code&gt;。这部分功能在代码&lt;code&gt;pcache.c&lt;/code&gt;中。&lt;/li&gt;&#xA;&lt;li&gt;页面缓存算法：用户可自己定制，只要实现&lt;code&gt;sqlite3_pcache_methods2&lt;/code&gt;结构体中的接口即可。系统中的默认实现，在文件&lt;code&gt;pcache1.c&lt;/code&gt;中。&lt;/li&gt;&#xA;&lt;li&gt;除此以外，还需要快速根据页面编号就能知道哪些页面已经被缓存的功能，这部分sqlite使用位图数据结构来实现，在文件&lt;code&gt;bitvec.c&lt;/code&gt;中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;页面缓存管理器，核心功能就是维护脏页面链表，缓存页面的管理，诸如根据页面编号查找页面、淘汰页面算法等，都由“页面缓存算法”来维护。可以这样来简单的理解上面的功能划分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;“页面缓存管理器”：定义了管理页面缓存的接口、总体流程，维护管理目前在用的脏页面。&lt;/li&gt;&#xA;&lt;li&gt;“页面缓存算法”：维护其它不在使用但还在内存中的页面，负责其淘汰、回收等实现。由“sqlite3_pcache_methods2”结构体实现，用户可以定制自己实现的“sqlite3_pcache_methods2”，系统也提供默认的实现。当内存不足以分配时，需要淘汰不常用的页面，这时候需要使用“页面缓存管理器”注册的回调函数来淘汰页面。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;页面缓存两个模块的功能划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-1-pagecache/pagecache_module.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 页面缓存两个模块的功能划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;简而言之，如果把当前在内存中的页面划分为以下两类，那么：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;当前在使用的页面：即与页面编号对应的页面，由“页面缓存管理器”维护。&lt;/li&gt;&#xA;&lt;li&gt;当前还未使用、但也在内存中的页面：即随时准备拿出来存储从磁盘中读出来的数据的页面，由“页面缓存算法”维护，比如淘汰、回收、复用等。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;两种页面的划分&#34; src=&#34;https://www.codedump.info/media/imgs/20211217-sqlite-btree-1-pagecache/page_cache_memory.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 两种页面的划分 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;下面，就开始“页面缓存”这几部分功能的具体讲解。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;管理页面&#34;&gt;&#xA;  管理页面&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%a1%e7%90%86%e9%a1%b5%e9%9d%a2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;页面相关的数据数据结构&#34;&gt;&#xA;  页面相关的数据数据结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%a1%b5%e9%9d%a2%e7%9b%b8%e5%85%b3%e7%9a%84%e6%95%b0%e6%8d%ae%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先来看页面相关的数据结构，sqlite中使用&lt;code&gt;PgHdr&lt;/code&gt;结构体来在内存中描述一个页面：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/*&#xA;** Every page in the cache is controlled by an instance of the following&#xA;** structure.&#xA;*/&#xA;struct PgHdr {&#xA;  sqlite3_pcache_page *pPage;    /* Pcache object page handle */&#xA;  void *pData;                   /* Page data */&#xA;  void *pExtra;                  /* Extra content */&#xA;  PCache *pCache;                /* PRIVATE: Cache that owns this page */&#xA;  PgHdr *pDirty;                 /* Transient list of dirty sorted by pgno */&#xA;  Pager *pPager;                 /* The pager this page is part of */&#xA;  Pgno pgno;                     /* Page number for this page */&#xA;#ifdef SQLITE_CHECK_PAGES&#xA;  u32 pageHash;                  /* Hash of page content */&#xA;#endif&#xA;  u16 flags;                     /* PGHDR flags defined below */&#xA;&#xA;  /**********************************************************************&#xA;  ** Elements above, except pCache, are public.  All that follow are &#xA;  ** private to pcache.c and should not be accessed by other modules.&#xA;  ** pCache is grouped with the public elements for efficiency.&#xA;  */&#xA;  i16 nRef;                      /* Number of users of this page */&#xA;  PgHdr *pDirtyNext;             /* Next element in list of dirty pages */&#xA;  PgHdr *pDirtyPrev;             /* Previous element in list of dirty pages */&#xA;                          /* NB: pDirtyNext and pDirtyPrev are undefined if the&#xA;                          ** PgHdr object is not dirty */&#xA;};&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中的信息，大部分在注释中已经自解释：&lt;/p&gt;</description>
    </item>
    <item>
      <title>sqlite3.36版本 btree实现（零）- 起步及概述</title>
      <link>https://www.codedump.info/zh/post/20211217-sqlite-btree-0/</link>
      <pubDate>Fri, 17 Dec 2021 10:19:05 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211217-sqlite-btree-0/</guid>
      <description>&lt;p&gt;《sqlite3.36版本 btree实现》系列文章：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/&#34;&gt;sqlite3.36版本 btree实现（零）- 起步及概述 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-1-pagecache/&#34;&gt;sqlite3.36版本 btree实现（一）- 管理页面缓存 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211218-sqlite-btree-2-concurrency-control/&#34;&gt;sqlite3.36版本 btree实现（二）- 并发控制框架 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20211222-sqlite-btree-3-journal/&#34;&gt;sqlite3.36版本 btree实现（三）- journal文件备份机制 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220106-sqlite-btree-4-wal/&#34;&gt;sqlite3.36版本 btree实现（四）- WAL的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20220201-sqlite-btree-5-btree/&#34;&gt;sqlite3.36版本 btree实现（五）- Btree的实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;起步&#34;&gt;&#xA;  起步&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%b5%b7%e6%ad%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在去年大体把btree以及b+tree算法流程研究了之后，我写了两篇博客：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下） - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;（鉴于b+tree只是btree的一个特例，下面描述将仅使用“btree”，不再严格区分两者。）&lt;/p&gt;&#xA;&lt;p&gt;但是，这两篇文章仅仅只是让我懂得了最基本的原理。懂得原理，只是能做出toy级别的实现，拿btree类的存储引擎来说，要做到生产级产品，至少还有以下几个问题我当时不知道怎么做的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如何处理不同大小的数据的存储？&lt;/li&gt;&#xA;&lt;li&gt;删除一个数据之后，如何复用其留下的空间？&lt;/li&gt;&#xA;&lt;li&gt;错误、崩溃恢复怎么做？&lt;/li&gt;&#xA;&lt;li&gt;跟磁盘文件是如何交互的？&lt;/li&gt;&#xA;&lt;li&gt;页面缓存模块如何实现？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;等等等等，还有太多我还没有弄清楚的实现细节。&lt;/p&gt;&#xA;&lt;p&gt;（我甚至还在微博上发问，得到了两个质量很高的回答，见本文最后的&lt;a href=&#34;https://www.codedump.info/post/20211217-sqlite-btree-0/#%E5%BD%A9%E8%9B%8B&#34;&gt;彩蛋部分&lt;/a&gt;。）&lt;/p&gt;&#xA;&lt;p&gt;对LSM类存储引擎有了解的人都知道，Leveldb这个项目在LSM领域属于入门级别的生产级实现，即这个领域最精简、但是又能放心在某些要求不高的场景下用于生产的项目。在这之后，我一直在找那种btree领域的“leveldb”，很遗憾一直都没有找到，我分别看了目前WiredTiger、innodb、sqlite的对应实现，都太复杂了，看不下去。&lt;/p&gt;&#xA;&lt;p&gt;直到有一天，无意间发现了这个项目：&lt;a href=&#34;https://github.com/madushadhanushka/simple-sqlite&#34;&gt;madushadhanushka/simple-sqlite: Code reading for sqlite backend&lt;/a&gt;，看介绍，作者把sqlite2.5里b-tree相关的部分代码抽取出来了，我编译运行了一下用例都能正常跑，代码量不过几千行，我只花了几天就看完了。&lt;/p&gt;&#xA;&lt;p&gt;虽然按照&lt;a href=&#34;https://www.sqlite.org/changes.html&#34;&gt;Release History Of SQLite&lt;/a&gt;上的记载，sqlite 2.5版本是2002年的版本了，但是这个版本还是某种程度回答了我在上面的疑问。&lt;/p&gt;&#xA;&lt;p&gt;趁热打铁，我又找来更新一些的sqlite 3.6.10代码继续看这部分的实现，这次花了更多的时间才看完，但是又增强了我的信心。由于这个版本的sqlite，还未实现btree的wal，还只是用了journal文件来做崩溃恢复（无论wal还是journal，都会在后面文章展开详细讨论），所以在有足够的信心之后，我接下来又继续看当时（2021.10月份）最新的sqlite 3.36版本的实现，这部分的实现对比3.6.10来说，在btree部分最大的变化就是多了wal的实现，在已经清楚3.6.10的前提下，再增加了解这部分的实现，也并不是什么难事了。&lt;/p&gt;&#xA;&lt;p&gt;以上，简单描述了我探索一个生产级btree实现的初过程，btree类存储引擎的实现博大精深，更复杂者还有很多（WiredTiger、innodb、tokudb&amp;hellip;），但是无疑从低版本sqlite开始的探索流程，终于让我打开了走上这条路的一扇大门。&lt;/p&gt;&#xA;&lt;p&gt;本系列文章就sqlite 3.36版本的btree实现展开描述，希望对那些和我一样对“生产级btree类存储引擎实现”有好奇心的人有一点帮助。&lt;/p&gt;&#xA;&lt;p&gt;当然，如果你还是觉得吃力，可以先从&lt;a href=&#34;https://github.com/madushadhanushka/simple-sqlite&#34;&gt;madushadhanushka/simple-sqlite: Code reading for sqlite backend&lt;/a&gt;这里看起。这里并不建议对btree原理没有了解的人直接上手sqlite的实现，如果需要了解原理请参考相关文章或者我上面给出的我写的两篇博客。这系列文章中，将不再对btree原理做过多描述，将假设读者已经了解这部分内容。&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（四）</title>
      <link>https://www.codedump.info/zh/post/20200726-boltdb-4/</link>
      <pubDate>Sun, 26 Jul 2020 17:55:11 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200726-boltdb-4/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;前面的章节中，分别讲解了boltdb的页面结构、Bucket结构以及事务相关的逻辑，最后一节讲解boltdb如何实现MVCC。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;mvcc概述&#34;&gt;&#xA;  MVCC概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mvcc%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;数据库的&lt;code&gt;ACID&lt;/code&gt;特性中，&lt;code&gt;Isolation&lt;/code&gt;即隔离性是一个较难实现的特性。&lt;/p&gt;&#xA;&lt;p&gt;一个数据库被修改时，在这次事务提交之前，不希望其他事务操作读到修改的结果。一种常见的办法就是加锁，但是锁的粒度如果很大，就会影响数据库的并发性能，即在写操作完成之前不能进行其他操作。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;MVCC（Multiversion concurrency control，多版本并发控制）&lt;/code&gt;是解决这个问题的一种方式。它的做法是：保存数据库中的多个版本，修改的是一个版本，而同时进行的读操作读取到的数据是旧版本的数据，这样即便读到了旧的数据也不影响，只要不是写操作中间的数据就好。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;readCommitted&#34; src=&#34;https://www.codedump.info/media/imgs/20200726-boltdb-4/readCommitted.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; readCommitted &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图所示，事务A修改了x的值为2，在这个事务提交之前，读事务B读取到的还是修改之前的值1，因为存在有两个该数据的不同版本，并且并没有因为有写操作同时存在而必须等待写操作完成才能进行读操作。在事务A提交之后，才能读到新的值2。（但是这个图里还有另外的问题，即同一个读事务的过程中，前后读到了同一个数据两次不同的值，这叫“不可重复读”，这就是另外一个问题了不在这里展开讨论。）&lt;/p&gt;&#xA;&lt;p&gt;本节讲解boltdb如何实现&lt;code&gt;MVCC&lt;/code&gt;操作，在开始讨论之前先看看boltdb如何管理数据库文件的。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;映射文件的使用&#34;&gt;&#xA;  映射文件的使用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%98%a0%e5%b0%84%e6%96%87%e4%bb%b6%e7%9a%84%e4%bd%bf%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb通过&lt;code&gt;mmap&lt;/code&gt;系统调用将数据库文件映射到内存中，64位体系下一个进程的虚拟内存空间有128TB，足够映射一个文件了。在把磁盘文件映射到内存之后，对磁盘文件的读写可以直接使用读写内存的操作，由操作系统内核来决定什么时候将哪部分的虚拟内存换入、换出物理内存。&lt;/p&gt;&#xA;&lt;p&gt;看上去这个好像方便了很多，其实并不建议在存储引擎中使用内存映射这样“偷懒”的技术。原因在于：操作系统内核除了知道什么时候换入换出物理内存之外，对数据库的其他操作一无所知，并不能很精准的控制其行为。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;db-mmap&#34; src=&#34;https://www.codedump.info/media/imgs/20200726-boltdb-4/db-mmap.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; db-mmap &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;对数据库进行内存映射的操作在函数&lt;code&gt;db.mmap&lt;/code&gt;函数中进行，大体流程如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;func&lt;/span&gt; (db *DB) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;mmap&lt;/span&gt;(minsz &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt;) &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;error&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;db.mmaplock.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Lock&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;defer&lt;/span&gt; db.mmaplock.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Unlock&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 计算至少要多大的文件大小才能满足minsz需求&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// Memory-map the data file as a byte slice.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;if&lt;/span&gt; err := &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;mmap&lt;/span&gt;(db, size); err != &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;nil&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; err&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这个函数只有在以下两种情况会被调用到：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;db初始化时，即初次加载db文件到内存映射中。&lt;/li&gt;&#xA;&lt;li&gt;当前文件不够大，需要进行扩容时，即在&lt;code&gt;db.allocate&lt;/code&gt;中分配新页面而当前页面不足需要扩充文件大小时。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;后者也被称为&lt;code&gt;remmap&lt;/code&gt;操作，即以新的大小重新映射文件进行内存中。为了避免每次增加了文件大小都需要重新进行文件内存映射操作，实际上boltdb是对文件大小做了&lt;code&gt;over allocate&lt;/code&gt;操作，具体的计算新文件大小的逻辑在函数&lt;code&gt;DB.mmapSize&lt;/code&gt;函数中实现的，这里不做展开了。&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（三）</title>
      <link>https://www.codedump.info/zh/post/20200725-boltdb-3/</link>
      <pubDate>Sat, 25 Jul 2020 11:26:33 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200725-boltdb-3/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;在前面的文章里，分别介绍了boltdb的几种页面格式、Bucket以及Cursor结构，本文介绍boltdb的事务（Transaction）。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb支持事务的&lt;code&gt;ACID&lt;/code&gt;特性，使用&lt;code&gt;MVCC&lt;/code&gt;来做并发控制，同时可以执行一个写事务和多个读事务：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;原子性（Atomicity）：未提交的写事务操作都在内存中。在提交写事务的时候，按照B+树数据、freelist、meta元数据的顺序写入文件。在meta元信息写入之前，都可以进行回滚（rollback）操作，只有meta元信息写入成功才能认为写操作执行成功。&lt;/li&gt;&#xA;&lt;li&gt;隔离性（Isolation）：每个读事务开始的时候获得一个版本号，读事务涉及到的页面不会被同时进行的写事务所覆盖；而每次写事务都会更新一个版本号。&lt;/li&gt;&#xA;&lt;li&gt;持久性（Durability）：写事务在提交的时候，会将这次写操作修改的数据（dirty page）分配新的页面，写入文件持久化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;本节首先讲解boltdb的事务基本实现，下一节讲解boltdb事务如何实现&lt;code&gt;MVCC&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;事务初始化&#34;&gt;&#xA;  事务初始化&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%ba%8b%e5%8a%a1%e5%88%9d%e5%a7%8b%e5%8c%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb中，任何一次读写操作，都有一个事务与之对应。这时候首先会调用&lt;code&gt;DB.Begin&lt;/code&gt;函数返回一个事务，而传入的参数会根据情况分别创建写和读事务：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;func&lt;/span&gt; (db *DB) &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Begin&lt;/span&gt;(writable &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bool&lt;/span&gt;) (*Tx, &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;error&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;if&lt;/span&gt; writable {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; db.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;beginRWTx&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;return&lt;/span&gt; db.&lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;beginTx&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到，根据是否是写事务，会分别调用&lt;code&gt;beginRWTx&lt;/code&gt;和&lt;code&gt;beginTx&lt;/code&gt;来创建读写事务和只读事务。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;DB&lt;/code&gt;结构体中，仅有一个写事务成员，而读事务则可以同时存在多个，因此同一个时间只能有一个写事务：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; DB &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;rwtx     *Tx&#x9;&#x9;&#x9;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 同一时间只能有一个未完成的写事务&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;txs      []*Tx&#x9;&#x9;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 保存未完成的读事务的，读事务可以有多个，写事务一个时间只能有一个，就在rwtx里面&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}  &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;beginTx&#34; src=&#34;https://www.codedump.info/media/imgs/20200725-boltdb-3/beginTx.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; beginTx &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;了解了在&lt;code&gt;DB&lt;/code&gt;中如何使用事务，下面来看事务结构体的定义：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; Tx &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;writable       &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bool&lt;/span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 是否写事务&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;managed        &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bool&lt;/span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;//&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;db             *DB&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 对应的db&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;meta           *meta&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 对应的meta数据指针&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;root           Bucket&#x9;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;pages          &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;map&lt;/span&gt;[pgid]*page&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 涉及到的page&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;stats          TxStats&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;commitHandlers []&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;func&lt;/span&gt;()&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// commit回调函数数组&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;WriteFlag &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;成员释义如下：&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（二）</title>
      <link>https://www.codedump.info/zh/post/20200711-boltdb-2/</link>
      <pubDate>Sat, 11 Jul 2020 09:33:06 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200711-boltdb-2/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20200625-boltdb-1/&#34;&gt;上一节&lt;/a&gt;里面，系统的介绍了Boltdb中几种类型页面的格式，有了这些基础，本节开始介绍boltdb中的Bucket结构。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;bucket&#34;&gt;&#xA;  Bucket&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#bucket&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在上一节中，Bucket类比于mysql中的table，在boltdb中，&lt;code&gt;meta&lt;/code&gt;页面中有一个成员&lt;code&gt;bucket&lt;/code&gt;，其存储了整个数据库根bucket的信息，而一个数据库中存储的其他table的信息，则作为子bucket存储到Bucket中。这几个数据结构的关系如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; DB &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;meta0    *meta&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;meta1    *meta  &#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; meta &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;root     bucket&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 根bucket的信息&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; Bucket &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;*bucket&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  buckets  &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;string&lt;/span&gt;]*Bucket &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 存储子bucket的对应关系&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; bucket &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 根节点的page id&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;root pgid &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// page id of the bucket&amp;#39;s root-level page&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 单调递增的序列号&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;sequence &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint64&lt;/span&gt; &lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// monotonically incrementing, used by NextSequence()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在&lt;code&gt;bucket&lt;/code&gt;数据结构中，两个成员的作用是：&lt;/p&gt;</description>
    </item>
    <item>
      <title>boltdb 1.3.0实现分析（一）</title>
      <link>https://www.codedump.info/zh/post/20200625-boltdb-1/</link>
      <pubDate>Thu, 25 Jun 2020 21:43:17 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200625-boltdb-1/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;本文基于boltdb 1.3.0对其实现进行分析。boltdb是etcd系统存储数据使用的KV嵌入式DB，使用Go编码实现，内部是一个B+树结构体。关于etcd、raft协议以及B+树，可以参考之前的文章：&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;B树、B+树索引算法原理（上）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20200615-btree-2/&#34;&gt;B树、B+树索引算法原理（下）&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/blockquote&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;本文的写作，主要参考了&lt;a href=&#34;https://www.jianshu.com/p/b86a69892990&#34;&gt;《区块的持久化之BoltDB》系列文章&lt;/a&gt;以及&lt;a href=&#34;https://youjiali1995.github.io/storage/boltdb&#34;&gt;boltdb 源码分析&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;boltdb是etcd项目使用的kv存储引擎，代码量不大，不算测试用例的话仅有几千行代码量，是入门存储引擎不错的参考项目。&lt;/p&gt;&#xA;&lt;p&gt;boltdb中与mysql这类的关系数据库相对应的概念列举如下：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;boltdb&lt;/th&gt;&#xA;          &lt;th&gt;mysql&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;db&lt;/td&gt;&#xA;          &lt;td&gt;database&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;bucket&lt;/td&gt;&#xA;          &lt;td&gt;table&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;即：在boltdb中，db代表一个数据库，对应一个db文件；而一个数据库中可能有多个表，对应的概念就是boltdb中的bucket。&lt;/p&gt;&#xA;&lt;p&gt;另外，对B+树有了解的都知道，B+树中为了减少磁盘读写次数，每次读写都是以页为单位的，对应到boltdb中用&lt;code&gt;page&lt;/code&gt;数据结构表示，&lt;code&gt;page&lt;/code&gt;只是描述磁盘上一个页面的数据结构，当一个页面读取到内存中时，就使用&lt;code&gt;node&lt;/code&gt;结构体来描述。另外，既然落地到磁盘的单位是页，就需要有数据结构来管理页面的分配，这部分使用&lt;code&gt;freelist&lt;/code&gt;这个数据结构来管理。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;page-struct&#34; src=&#34;https://www.codedump.info/media/imgs/20200625-boltdb-1/page-struct.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; page-struct &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;以下，首先展开对页面相关核心数据结构的分析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;数据库文件的磁盘布局和页面&#34;&gt;&#xA;  数据库文件的磁盘布局和页面&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b0%e6%8d%ae%e5%ba%93%e6%96%87%e4%bb%b6%e7%9a%84%e7%a3%81%e7%9b%98%e5%b8%83%e5%b1%80%e5%92%8c%e9%a1%b5%e9%9d%a2&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面提到过，boltdb中以页面为单位来进行磁盘的读写操作，一个页面的大小一般而言与操作系统的页面一致，即4K大小。在boltdb中，分为以下几种类型的页面：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;存储meta元数据的页面。&lt;/li&gt;&#xA;&lt;li&gt;存储freelist，即管理页面数据的页面。&lt;/li&gt;&#xA;&lt;li&gt;Branch页面，存储B+树索引节点，也就是内部节点的页面。&lt;/li&gt;&#xA;&lt;li&gt;Leaf页面，存储B+树数据节点，也就是叶子节点的页面。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;boltdb代码中定义页面类型如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;const&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;branchPageFlag   = 0x01&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;leafPageFlag     = 0x02&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;metaPageFlag     = 0x04&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;freelistPageFlag = 0x10&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这四种页面，在boltdb的数据库文件的布局大体如下：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;boltdb-layout&#34; src=&#34;https://www.codedump.info/media/imgs/20200625-boltdb-1/boltdb-layout.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; boltdb-layout &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;从上图中可以看出：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;最开始的两个页面是两个meta页面，至于为什么是两个，后面再展开讨论。&lt;/li&gt;&#xA;&lt;li&gt;紧跟着的一个页面是freelist页面。&lt;/li&gt;&#xA;&lt;li&gt;从上面可知，数据库文件中最开始的三个页面存的都是管理信息，此后数据数据型的branch以及leaf页面了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;接下来就这几种页面具体的结构展开说明，不过在此之前还是首先来看看&lt;code&gt;page&lt;/code&gt;结构体，它用于表示一个磁盘页面的数据结构。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;page结构体&#34;&gt;&#xA;  page结构体&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#page%e7%bb%93%e6%9e%84%e4%bd%93&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;page&lt;/code&gt;结构体的定义如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-Go&#34; data-lang=&#34;Go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; pgid &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint64&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;type&lt;/span&gt; page &lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id       pgid&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    flags    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint16&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    count    &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint16&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    overflow &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint32&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ptr      &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uintptr&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中：&lt;/p&gt;</description>
    </item>
    <item>
      <title>B树、B&#43;树索引算法原理（下）</title>
      <link>https://www.codedump.info/zh/post/20200615-btree-2/</link>
      <pubDate>Mon, 15 Jun 2020 22:44:21 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200615-btree-2/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这一段时间由于在阅读boltdb代码的缘故，找机会学习了B树及B+树的算法原理，这个系列会花两个篇幅分别介绍这两种数据结构的实现，其用于数据库索引中的基本原理。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;在&lt;a href=&#34;https://www.codedump.info/post/20200609-btree-1/&#34;&gt;上一篇文章&lt;/a&gt;中，介绍了数据库索引的简单概念，以及B树的结构及核心算法，这一篇将继续介绍B树的变形B+树。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树的定义及性质&#34;&gt;&#xA;  B+树的定义及性质&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e7%9a%84%e5%ae%9a%e4%b9%89%e5%8f%8a%e6%80%a7%e8%b4%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;B+树之于B树，最大的不同在于：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;B树的数据可以存储在内部节点上，也可以存储在叶子节点上。&lt;/li&gt;&#xA;&lt;li&gt;而在B+树中，内部节点上仅存放数据的索引，数据只存储在叶子节点上。在内部节点中的键值，被称为“索引”，由于是数据索引，因此可能出现同一个键值，既出现在内部节点，也出现在叶子节点中的情况。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;内部节点的“索引”，应该满足以下条件：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;大于左边子树的最大键值；&lt;/li&gt;&#xA;&lt;li&gt;小于等于右边子树的最小键值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;同时，B+树为了方便范围查询，叶子节点之间还用指针串联起来。&lt;/p&gt;&#xA;&lt;p&gt;以下是一颗B+树的典型结构：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;b&amp;#43;tree&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/b&amp;#43;tree.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; b+tree &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于采用了这样的结构，B+树对比B树有以下优点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;索引节点上由于只有索引而没有数据，所以索引节点上能存储比B树更多的索引，这样树的高度就会更矮。按照我们上一篇中介绍数据库索引的内容，这种面向磁盘的数据结构，树的高度越矮，磁盘寻道的次数就会越少。&lt;/li&gt;&#xA;&lt;li&gt;因为数据都集中在叶子节点了，而所有叶子节点的高度相同，那么可以在叶子节点中增加前后指针，指向同一个父节点的相邻兄弟节点，给范围查询提供遍历。比如这样的SQL语句：&lt;code&gt;select * from tbl where t &amp;gt; 10&lt;/code&gt;，如果使用B+树存储数据的话，可以首先定位到数据为10的节点，再沿着它的next指针一路找到所有在该叶子节点右边的叶子节点数据返回。而如果使用B树结构，由于数据既可以存储在内部节点也可以存储在叶子节点，范围查询可想而知是很繁琐的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;核心算法&#34;&gt;&#xA;  核心算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a0%b8%e5%bf%83%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;插入算法&#34;&gt;&#xA;  插入算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8f%92%e5%85%a5%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;B+树的插入算法与B树的很相近，都是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先判断待插入数据节点是否已经溢出，如果是就首先拆分成两个节点，然后再插入数据。&lt;/li&gt;&#xA;&lt;li&gt;由于内部节点上的数据是索引，所以在插入完成之后调整父节点指针。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如在下图的B+树中，向这里插入新的数据&lt;code&gt;10&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide01b&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide01b.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide01b &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于插入节点&lt;code&gt;[7,11]&lt;/code&gt;在插入之后并没有溢出，所以可以直接变成&lt;code&gt;[7,10,11]&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide01c&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide01c.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide01c &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;而如下图的B+树中，插入数据&lt;code&gt;4&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide02b&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide02b.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide02b &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;由于所在节点&lt;code&gt;[2,3,5]&lt;/code&gt;在插入之后数据溢出，因此需要分裂为两个新的节点，同时调整父节点的索引数据：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;slide02g&#34; src=&#34;https://www.codedump.info/media/imgs/20200615-btree-2/slide02g.gif&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; slide02g &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;[2,3,4,5]&lt;/code&gt;分裂成了&lt;code&gt;[2,3]&lt;/code&gt;和&lt;code&gt;[4,5]&lt;/code&gt;，因此需要在这两个节点之间新增一个索引值，这个值应该满足：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;大于左子树的最大值。&lt;/li&gt;&#xA;&lt;li&gt;小于等于右子树的最小值。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;综上，需要在父节点中新增索引&lt;code&gt;4&lt;/code&gt;和两个指向新节点的指针。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;删除算法&#34;&gt;&#xA;  删除算法&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%a0%e9%99%a4%e7%ae%97%e6%b3%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;B+树的删除算法，与B树类似，分为以下几步：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;首先查询到键值所在的叶子节点，删除该叶子节点的数据。&lt;/li&gt;&#xA;&lt;li&gt;如果删除叶子节点之后的数据数量，满足B+树的平衡条件，则直接返回不用往下走了。&lt;/li&gt;&#xA;&lt;li&gt;否则，就需要做平衡操作：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果该叶子节点的左右兄弟节点的数据量可以借用，就借用过来满足平衡条件。&lt;/li&gt;&#xA;&lt;li&gt;否则，就只能与相邻的兄弟节点合并成一个新的子节点了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;在上面平衡操作中，如果是进行了合并操作，就需要向上修正父节点的指针：删除被合并节点的键值以及指针。由于做了删除操作，可能父节点也会不平衡，那么就按照前面的步骤也对父节点进行重新平衡操作，这样一直到某个节点平衡为止。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面结合&lt;a href=&#34;http://www.mathcs.emory.edu/~cheung/Courses/554/Syllabus/3-index/B-tree=delete1.html&#34;&gt;B-tree=delete1&lt;/a&gt;、&lt;a href=&#34;http://www.mathcs.emory.edu/~cheung/Courses/554/Syllabus/3-index/B-tree=delete2.html&#34;&gt;B-tree=delete2&lt;/a&gt; 的图示对删除算法展开具体的分析。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;从叶子节点中删除数据&#34;&gt;&#xA;  从叶子节点中删除数据&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%bb%8e%e5%8f%b6%e5%ad%90%e8%8a%82%e7%82%b9%e4%b8%ad%e5%88%a0%e9%99%a4%e6%95%b0%e6%8d%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;从叶子节点中删除数据分为三种情况：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;删除之后的数据量足够，不需要进行重平衡操作；&lt;/li&gt;&#xA;&lt;li&gt;删除之后的数据量不够，但是可以从兄弟节点那里借用数据，重新达到平衡；&lt;/li&gt;&#xA;&lt;li&gt;删除之后的数据量不够，兄弟节点的数据也不够，那么需要合并成一个新的节点，同时在父节点中删除索引和指针。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下针对后面两种需要做重平衡的操作展开分析。&lt;/p&gt;&#xA;&lt;h4 class=&#34;heading&#34; id=&#34;借用兄弟节点数据进行重平衡操作&#34;&gt;&#xA;  借用兄弟节点数据进行重平衡操作&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%80%9f%e7%94%a8%e5%85%84%e5%bc%9f%e8%8a%82%e7%82%b9%e6%95%b0%e6%8d%ae%e8%bf%9b%e8%a1%8c%e9%87%8d%e5%b9%b3%e8%a1%a1%e6%93%8d%e4%bd%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h4&gt;&#xA;&lt;p&gt;在下图中，从叶子节点中删除数据之后，只剩下数据&lt;code&gt;[11]&lt;/code&gt;：&lt;/p&gt;</description>
    </item>
    <item>
      <title>B树、B&#43;树索引算法原理（上）</title>
      <link>https://www.codedump.info/zh/post/20200609-btree-1/</link>
      <pubDate>Tue, 09 Jun 2020 18:40:46 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20200609-btree-1/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这一段时间由于在阅读boltdb代码的缘故，找机会学习了B树及B+树的算法原理，这个系列会花两个篇幅分别介绍这两种数据结构的实现，其用于数据库索引中的基本原理。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树数据库索引原理&#34;&gt;&#xA;  B树数据库索引原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e6%95%b0%e6%8d%ae%e5%ba%93%e7%b4%a2%e5%bc%95%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在一堆数据中查找一个数据时，常用的数据结构有二叉查找树（binary search tree，简称BST）、哈希桶等。以BST为例，常见的实现有AVT、红黑树等，由于这类型的树是平衡的，每次比较操作都会去掉当前数据量一半的数据，因此查找的时间复杂度为&lt;code&gt;O(log2n)&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;bst-example&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/bst-example.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; bst-example &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;但是这类型数据结构的问题在于，由于每个节点只能容纳一个数据，导致树的高度很高，逻辑上挨着的节点数据可能离的很远。如果是在内存中操作数据的话，这样问题并不大。&lt;/p&gt;&#xA;&lt;p&gt;考虑在磁盘中存储数据的情况，与内存相比，读写磁盘有以下不同点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读写磁盘的速度相比内存读写慢很多。&lt;/li&gt;&#xA;&lt;li&gt;因为上面的原因，因此每次读写磁盘的单位要比读写内存的最小单位大很多。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因为读写磁盘的这个特点，因此对应的数据结构应该尽量的满足“局部性原理”：“当一个数据被用到时，其附近的数据也通常会马上被使用”，为了满足局部性原理，应该：&lt;strong&gt;将逻辑上相邻的数据在物理上也尽量存储在一起&lt;/strong&gt;。这样才能减少读写磁盘的数量。&lt;/p&gt;&#xA;&lt;p&gt;所以，对比起一个节点只能存储一个数据的BST类数据结构来，要求这种数据结构在形状上更“胖”、更加“扁平”，即：每个节点能容纳更多的数据，这样就能降低树的高度，同时让逻辑上相邻的数据都能尽量的存储在物理上也相邻的硬盘空间上，减少磁盘读写。&lt;/p&gt;&#xA;&lt;p&gt;以下图为例：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;disk-ds&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/disk-ds.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; disk-ds &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;图中从根节点出发，查找数据14的过程中，经过的第二个节点中有键值&lt;code&gt;[3,7,13]&lt;/code&gt;，这三个值在“逻辑”上是相邻的，如果它们在磁盘上的存储也能做到在“物理”上相邻，那么只需要一次读操作就能把这个节点的数据从磁盘上加载到内存中进行数据比较，这样整个查找过程就只需要两次磁盘读操作。&lt;/p&gt;&#xA;&lt;p&gt;在这里，一个节点越“胖”，意味着扇出（fanout）越大，同时高度越低，这两个性质决定了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;高扇出：邻近键值的数据局部性更好。&lt;/li&gt;&#xA;&lt;li&gt;低高度：遍历期间的寻道次数更少。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以证明，查找数据的次数（searchnum）与degree、以及数据总量有以下关系：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree-num&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/btree-num.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree-num &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;B树和B+树就是两种利用磁盘局部性原理进行优化的树结构，B+树基于B树做了一些改进，这里首先将介绍B树的原理。本系列将用两篇文章讲解这两种数据结构的原理，并且提供Python实现代码。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树的定义及性质&#34;&gt;&#xA;  B树的定义及性质&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e7%9a%84%e5%ae%9a%e4%b9%89%e5%8f%8a%e6%80%a7%e8%b4%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在B树中，分为两种节点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;内部节点（internal node）：存储了数据以及指向其子节点的指针。&lt;/li&gt;&#xA;&lt;li&gt;叶子节点（leaf node）：与内部节点不同的是，叶子节点只存储数据，并没有子节点。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一个数据，既可能存在内部节点上，也可能存在叶子节点上，这一点是与后面讲到的B+树最大的不同，后者只会将数据存储在叶子节点上。&lt;/p&gt;&#xA;&lt;p&gt;创建B树时，需要输入一个degree参数（以下简写为t），该参数决定了每个节点上数据量的多少，即节点的“胖”、“瘦”程度，而节点的胖瘦程度又会影响整棵树的高度，因为越胖的节点树高度就会越矮。&lt;/p&gt;&#xA;&lt;p&gt;为了维持B树的平衡性，需要满足以下的属性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在每个节点上的键值，以递增顺序排列，即&lt;code&gt;node.keys[i] &amp;lt;= node.keys[i+1]&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;在一个键值左边的子树，其键值大于该键值右边子树的所有键值，即&lt;code&gt;node.keys[i] &amp;gt; max(node.child[i]的所有键值)&lt;/code&gt;；同时，在一个键值右边的子树，其键值的最小值都不小于该键值，即&lt;code&gt;node.keys[i] &amp;lt;= min(node.child[i + 1]的所有键值)&lt;/code&gt;。具体情况可以在下面的图中进行说明。&lt;/li&gt;&#xA;&lt;li&gt;在内部节点中，指向子节点的指针数量总是存储数据节点的数量+1，即：&lt;code&gt;num(node.child) = num(node.keys) + 1&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;所有叶子节点的高度一致。&lt;/li&gt;&#xA;&lt;li&gt;无论是内部节点还是叶子节点，其存储的键值数量在&lt;code&gt;[t-1,2t-1]&lt;/code&gt;之间，如果数量不满足此条件，需要做重平衡操作。如果少于&lt;code&gt;t-1&lt;/code&gt;，需要借用或合并数据；反之，如果数据量大于&lt;code&gt;2t-1&lt;/code&gt;，则需要分裂成两个节点。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我们来看下面的图示，该图中的B树，t参数的值为2（&lt;strong&gt;需要特别说明的是，一棵树中每个存储数据的地方，应该既有键值（key）也有数据（value），本文中为了简单起见，存储的数据只有键值。&lt;/strong&gt;）：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;btree-example&#34; src=&#34;https://www.codedump.info/media/imgs/20200609-btree-1/btree-example.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; btree-example &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;由于&lt;code&gt;t=2&lt;/code&gt;，所有所有节点的键值数量在&lt;code&gt;[1,3]&lt;/code&gt;之间。&lt;/li&gt;&#xA;&lt;li&gt;所有叶子节点的高度相同。&lt;/li&gt;&#xA;&lt;li&gt;以左边的内部节点为例，其第一个键值为3，即该节点的&lt;code&gt;keys[0]=3&lt;/code&gt;，而该键值的左边子树的键值为&lt;code&gt;[1,2]&lt;/code&gt;，都小于3，即&lt;code&gt;keys[0]&amp;gt;max(child[0]的所有键值)&lt;/code&gt;；而其右边子树的键值为&lt;code&gt;[4,5,6]&lt;/code&gt;，都不小于3，即&lt;code&gt;keys[0]&amp;lt;=min(child[1]的所有键值)&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;b树算法原理&#34;&gt;&#xA;  B树算法原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#b%e6%a0%91%e7%ae%97%e6%b3%95%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;了解了B树的性质，下面讨论B树中的两个核心操作：插入及删除。这两个操作的核心，都是在操作如果破坏了B树的平衡性之后，进行重新平衡以满足B树的性质。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;插入数据&#34;&gt;&#xA;  插入数据&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8f%92%e5%85%a5%e6%95%b0%e6%8d%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;向B树中插入一个数据，可能会导致节点的数据变满，即不满足上面提到的节点数据数量在&lt;code&gt;[t,2t-1]&lt;/code&gt;这个性质。此时需要对节点进行分裂节点操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将数据变满（即节点数据量为&lt;code&gt;2t&lt;/code&gt;）的节点，分为左右两个数据量分别为&lt;code&gt;t-1&lt;/code&gt;的节点，同时将中间的数据提升到父节点的合适位置上。&lt;/li&gt;&#xA;&lt;li&gt;如果父节点由于新增了这个被提升的数据导致了变满，就继续上面的分裂节点操作。&lt;/li&gt;&#xA;&lt;li&gt;沿着树向上一直执行该操作，直到不再变满为止。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;分裂操作的示意图如下：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Leveldb代码阅读笔记</title>
      <link>https://www.codedump.info/zh/post/20190215-leveldb/</link>
      <pubDate>Fri, 15 Feb 2019 08:52:47 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190215-leveldb/</guid>
      <description>&lt;p&gt;本文基于leveldb 1.9.0代码。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;整体架构&#34;&gt;&#xA;  整体架构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%95%b4%e4%bd%93%e6%9e%b6%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;leveldb&#34; src=&#34;https://www.codedump.info/media/imgs/20190215-leveldb/leveldb.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; leveldb &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图，leveldb的数据存储在内存以及磁盘上，其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;memtable：存储在内存中的数据，使用skiplist实现。&lt;/li&gt;&#xA;&lt;li&gt;immutable memtable：与memtable一样，只不过这个memtable不能再进行修改，会将其中的数据落盘到level 0的sstable中。&lt;/li&gt;&#xA;&lt;li&gt;多层sstable：leveldb使用多个层次来存储sstable文件，这些文件分布在磁盘上，这些文件都是根据键值有序排列的，其中0级的sstable的键值可能会重叠，而level 1及以上的sstable文件不会重叠。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在上面这个存储层次中，越靠上的数据越新，即同一个键值如果同时存在于memtable和immutable memtable中，则以memtable中的为准。&lt;/p&gt;&#xA;&lt;p&gt;另外，图中还使用箭头来表示了合并数据的走向，即：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-C&#34; data-lang=&#34;C&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;memtable -&amp;gt; immutable memtable -&amp;gt; level 0 sstable -&amp;gt; level 1 sstable -&amp;gt; ... -&amp;gt; level N sstable&lt;span style=&#34;&#34;&gt;。&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以下将针对这几部分展开讨论。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;log文件&#34;&gt;&#xA;  Log文件&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#log%e6%96%87%e4%bb%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;写入数据的时候，最开始会写入到log文件中，由于是顺序写入文件，所以写入速度很快，可以马上返回。&lt;/p&gt;&#xA;&lt;p&gt;来看Log文件的结构：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一个Log文件由多个Block组成，每个Block大小为32KB。&lt;/li&gt;&#xA;&lt;li&gt;一个Block内部又有多个Record组成，Record分为四种类型：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Full：一个Record占满了整个Block存储空间。&lt;/li&gt;&#xA;&lt;li&gt;First：一个Block的第一个Record。&lt;/li&gt;&#xA;&lt;li&gt;Last：一个Block的最后一个Record。&lt;/li&gt;&#xA;&lt;li&gt;Middle：其余的都是Middle类型的Record。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;Record的结构如下：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Header部分&#xA;&lt;ul&gt;&#xA;&lt;li&gt;32位长度的CRC Checksum：存储这个Record的数据校验值，用于检测Record合法性。&lt;/li&gt;&#xA;&lt;li&gt;16位长度的Length：存储数据部分长度。&lt;/li&gt;&#xA;&lt;li&gt;8位长度的Type：存储Record类型，就是上面说的四种类型。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;数据部分&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;log-file&#34; src=&#34;https://www.codedump.info/media/imgs/20190215-leveldb/log-file.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; log-file &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;memtable&#34;&gt;&#xA;  memtable&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#memtable&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;memtable用于存储在内存中还未落盘到sstable中的数据，这部分使用跳表（skiplist）做为底层的数据结构，这里先简单描述一下跳表的工作原理。&lt;/p&gt;&#xA;&lt;p&gt;如果数据存放在一个普通的有序链表中，那么查找数据的时间复杂度就是O(n)。跳表的设计思想在于：链表中的每个元素，都有多个层次，查找某一个元素时，遍历该链表的时候，根据层次来跳过（skip）中间某些明显不满足需求的元素，以达到加快查找速度的目的，如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;skiplist&#34; src=&#34;https://www.codedump.info/media/imgs/20190215-leveldb/skiplist.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; skiplist &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在以上这个跳表中，查找元素6的流程，大体如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;构建一个每个链表元素最多有5个元素的跳表。&lt;/li&gt;&#xA;&lt;li&gt;由于6大于链表的第一个元素1，因此如果存在必然在1之后的元素中，因此进入元素1的指针数组中，从上往下查找元素4：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;第一层：指向的指针为Nil空指针，不满足需求，继续往下查找；&lt;/li&gt;&#xA;&lt;li&gt;第二层：指向的指针保存的数据为4，小于待查找的元素4，因此如果元素6存在也必然在4之后，因此指针跳转到元素4所在的位置，继续从上往下开始查找。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;到了元素4所在的指针数组，开始从上往下继续查找：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;第一层：指向的指针保存的数据为6，查找完毕。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面的分析过程中可以看到：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;跳表是一种以牺牲更多的存储空间换取查找速度，即“空间换时间”的数据结构。&lt;/li&gt;&#xA;&lt;li&gt;跳表的每一层也都是一个有序链表。&lt;/li&gt;&#xA;&lt;li&gt;如果一个元素出现在第i层的链表中，那么也必然会在第i层以下的链表中出现。&lt;/li&gt;&#xA;&lt;li&gt;链表的每个节点中，垂直方向的数组存储的数据都是一样的，水平方向的指针指向链表的下一个元素。&lt;/li&gt;&#xA;&lt;li&gt;最底层的链表包含所有元素，也就是说，在最底层数据结构退化为一个普通的有序链表。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;sstable文件&#34;&gt;&#xA;  sstable文件&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sstable%e6%96%87%e4%bb%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;大体结构&#34;&gt;&#xA;  大体结构&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%a7%e4%bd%93%e7%bb%93%e6%9e%84&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先来看sstable文件的整体结构，如下图：&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
