H2 存储内核解析

简介: MVStore是“多版本存储”(Multi-Version Store)的缩写,是一种持久化的基于日志结构的键值存储。它是H2的默认存储引擎,支持SQL、JDBC、事务、MVCC等。但也可以直接在应用程序中使用,而不使用JDBC或SQL。

概述

MVStore是“多版本存储”(Multi-Version Store)的缩写,是一种持久化的基于日志结构的键值存储。它是H2的默认存储引擎,支持SQL、JDBC、事务、MVCC等。但也可以直接在应用程序中使用,而不使用JDBC或SQL。

以下是MVStore的特点:

  1. 内部包含多个Map,可以使用Java中的java.util.Map接口访问。
  2. 支持基于文件的持久化和内存操作,旨在快速、简单易用且小巧。
  3. 支持并发读写操作和事务(包括并发事务和两阶段提交)。
  4. 支持插件式数据类型和序列化、插件式存储(文件、离堆内存)、插件式Map实现(B树、R树、并发B树)、BLOB存储和文件系统抽象以支持加密文件和zip文件。

例子

String fileName = "/Users/chenfei/temp/my_store.db";
        // 
        FileUtils.delete(fileName);
        MVStore.Builder builder = new MVStore.Builder();
        builder.fileName(fileName);
        builder.pageSplitSize(1000);
        MVStore store = builder.open();
        MVMap<Integer, String> m = store.openMap("data");
        int count = 2;
        for (int i = 0; i < count; i++) {
            m.put(i, "hello " + i);
            System.out.println(m.get(i));
        }
        store.commit();
        store.close();

复制

文件格式(File)

文件(File)包含两个文件头和若干个数据块(Chunk)。每个数据块(Chunk)至少为一个块(Block),但通常为200个块(Block)或更多。数据以日志结构存储的形式存储在数据块(Chunk)中。每个版本(Version)都有一个数据块(Chunk)。

[ file header 1 ] [ file header 2 ] [ chunk ] [ chunk ] ... [ chunk ]

复制

文件头 (File Header)

文件头 (File Header):为了安全起见,文件头有两个,通常包含完全相同的数据。以便在写入期间部分失败时不会破坏文件头。只有文件头才支持原地更新"in-place update"。

文件头包含以下信息:

H:2,block:2,blockSize:1000,chunk:7,created:1441235ef73,format:1,version:7,fletcher:3044e6cc

复制

H

“H:2”代表 H2 数据库

块(block)

最新(不必是最新的)数据块(chunks)的起始块(block)号

块大小(blockSize)

文件块的块大小;当前始终为十六进制1000,即十进制4096,以匹配现代硬盘的磁盘扇区长度。

数据块(chunk)

数据块id,通常与版本号相同;但是,数据块id可能会回滚到0,而版本不会。

created

创建文件时距1970年以来的毫秒数

format

文件格式版本(当前为1)

version

文件版本

fletcher

校验和

在打开文件时,会读取两个文件头并验证校验和。如果两个文件头都有效,则使用版本更新的文件头。然后检测具有最新版本的数据块(chunk),并从其中读取其余元数据。如果文件头中没有数据块 ID,块(block)和版本,则最新数据块(chunk)的查找将从文件中的最后一个数据块(chunk)开始。

数据块格式(Chunk Format)

每个版本都有一个数据块(chunk),每个数据块(chunk)由一个 header、在此版本中修改的页面(page)和 footer组成。页面(page)包含以 map 形式的实际数据。数据块(chunk)中的页面(page)在 header 后紧挨着存储(未对齐)。数据块(chunk)的大小是块(block)大小的倍数。footer 存储在数据块(chunk)的最后128字节中。

[ header ] [ page ] [ page ] ... [ page ] [ footer ]

复制

chunk foot 用于验证chunk是否完整写入,以及查找文件中最后一个chunk的起始位置。chunk中的页面 (page) 存储着 map 形式的实际数据。chunk中的页面 (page) 存储在 header 的后面,相邻存放。chunk 的大小是 block 大小的倍数。每个 chunk 只包含在该版本中被修改的页面 (page) ,以及这些页面 (page) 的所有父节点,递归到根页面 (page) 。如果map中的条目被更改、删除或添加,则会复制相应的页面 (page)并在下一个chunk中存储修改后的页面 (page)。没有活页面 (page)的chunk被标记为空,以便更近期的chunk可以重复使用它们的空间。

chunk header

chunk:1,block:2,len:1,map:6,max:1c0,next:3,pages:2,root:4000004f8c,time:1fc,version:1

复制

chunk:1,block:2,version:1,fletcher:aed9a4f6

复制

chunk

chunk的ID

block

chunk的第一个block的编号(乘以block大小可以得到文件中的位置)

len

chunk包含的页面 (page)数

map

最新map的ID,每次创建新map时递增

max

chunk中页面(page)的最大数量

pages

chunk中页面(page)的数量

next

下一个chunk的ID

root

元数据根页面(page)的位置

time

chunk被写入的时间,从文件创建后的毫秒数开始计算

version

chunk的版本号

每个版本都有一个 chunk,chunk 永远不会被原地更新。每个 chunk 包含了在该版本中更改的页面(page)(每个版本有一个 chunk),以及递归地包含了所有这些页面(page)的父节点,一直到根页面(page)。如果 map 中的条目被更改、删除或添加,则相应的页面(page)将被复制、修改并存储在下一个 chunk 中,旧 chunk 中的活动页面(page)数将减少。这种机制被称为写时复制,类似于 Btrfs 文件系统的工作方式。那些没有活动页面的 chunks 被标记为空闲状态,因此空间可以被最近的 chunks 重复使用。因为不是所有的 chunk 都是相同大小的,所以在某段时间内,一些 chunk 前面可能会有一些空闲 blocks(直到写入小块或压缩块)。默认情况下,在空闲 blocks 被覆盖之前会有45秒的延迟,以确保新版本首先被持久化.

如何在打开存储时找到最新的 chunk:文件头包含最近chunk的位置,但不总是最新的chunk。这是为了减少文件头更新的次数。打开文件后,读取文件头和最后一个chunk的块尾。从这些候选chunk中,读取最新chunk的头。如果它包含一个“next”指针,则也读取这些chunk的头和尾。如果它是一个新的有效chunk,则重复这个过程,直到找到最新的chunk。在写入chunk之前,基于下一个chunk与当前chunk相同的假设,预测下一个chunk的位置。当写入下一个chunk并且前一个预测是不正确的时,文件头也会更新。在任何情况下,如果下一个链的跳数超过20次,则文件头将被更新。

页面格式(Page Format)

每个Map都是一棵B树,Map数据存储在(B树)页面中。页面(page)分为叶子页面和内部节点页面。叶子页面包含Map的键值对,而内部节点只包含键和指向叶子页面的指针。树的根节点可以是叶子页面或内部节点。不同于文件头,数据块 header和 foot 的数据,页面数据是存储为字节数组的,其中包含长整型(8个字节)、整型(4个字节)、短整型(2个字节)和可变大小的整型和长整型(1到5/10个字节)。

页面格式的参数

length

页面的字节数

checksum

计算方法为 chunk id 异或 page 在 chunk 中的偏移量 offset 异或 page 大小。

mapId

该页面所属 map 的 ID

len

该页面中 key 的数量。

type (byte)

页面的类型。叶节点:0。内部节点:1。

children

子节点位置 (long 类型数组;仅仅是内部节点)

childCounts

Child 页面的数量

keys

(字节数组)数组存储了该节点的所有键,类型取决于数据类型

values

(字节数组)(仅适用于叶子节点)存储了该节点的所有值,类型取决于数据类型

尽管文件格式不要求这样做,但页面(page)按以下顺序存储:对于每个映射,首先存储根页面(root page),然后是内部节点(如果有的话),然后是叶子页面(page)。metadata map 存储在chunk的末尾。

在MVStore中,页面(page)的指针以一个长整型的特殊格式存储:26位用于chunk ID,32位用于chunk 内偏移量,5位用于长度编码,1位用于页面(page)类型(叶节点或内部节点)。页面(page)类型被编码,以便在清除或删除映射时,不必读取叶节点页面(page)(内部节点必须读取,以便知道所有页面(page)的位置;但在典型的B树中,绝大多数页面(page)都是叶节点页面)。绝对文件位置不包括在内,以便可以在文件中移动chunk而无需更改页指针;只需要更改chunk元数据。 长度代码是从0到31的数字,其中0表示页面(page)的最大长度为32个字节,1表示48个字节,2表示64,3表示96,4表示128,5表示192,以此类推,直到31表示超过1 MB。这样,只需要一个读取操作即可读取页面(page)(除非是非常大的页面)。所有页面(page)的最大长度之和存储在chunk元数据中(字段“max”),当将页面标记为已删除时,会调整实时最大长度。这允许估计 block 内的可用空间量,以及可用页面的数量。

Metadata Map

在MVStore中,除了用户 map 之外,还有一个元数据map (metadata map),其中包含用户map的名称和位置以及chunk元数据。chunk的最后一页包含该元数据chunk的根页。该根页的确切位置存储在chunk头中。该页(直接或间接)指向所有其他map的根页。元数据map有一个名为 "data" 的map,它包含如下信息:

chunk.1

chunk 1 的元数据。这是与chunk头相同的数据,加上活动页面的数量和最大活动页面长度。

map.1

map 1的元数据。条目包括名称,创建版本和类型。

name.data

名为“data”的map的map ID。值为“1”

root.1

map 1 的根位置

setting.storeVersion

store版本(用户定义的值)

可以通过调用getMetaMap()方法来获取metadata

代码解析

生成 MVStore 对象

fileName 不存在的时,执行 writeStoreHeader(),存在的时候读取 readStoreHeader();

String fileName = "/Users/chenfei/temp/my_store.db";
        MVStore.Builder builder = new MVStore.Builder();
        builder.fileName(fileName);
        builder.pageSplitSize(1000);
        MVStore store = builder.open();

复制

MVStore 对象创建成功后,它就包括了如下属性:

网络异常,图片无法展示
|

获取 meta map

通过 MVStore 对象

MVMap<String, String> metaMap = store.getMetaMap();

复制

获取第一个 chunk

在 readStoreHeader 方法中的 readChunkHeaderAndFooter(block); 会通过 setLastChunk 方法设置 chunk。

/**
    * 获取最新的 chunk
    */
    public Chunk setLastChunk(Chunk last) {
        chunks.clear();
        lastChunk = last;
        if (last == null) {
            // no valid chunk
            lastMapId.set(0);
            currentVersion = 0;
            lastStoredVersion = MVMap.INITIAL_VERSION;
            meta.setRootPos(0, MVMap.INITIAL_VERSION);
        } else {
            lastMapId.set(last.mapId);
            currentVersion = last.version;
            chunks.put(last.id, last);
            lastStoredVersion = currentVersion - 1;
            meta.setRootPos(last.metaRootPos, lastStoredVersion);
        }
        return last;
    }

复制

获取 chunk 的根 page

String name = "my_data_1";
        MVMap.MapBuilder mapBuilder = new MVMap.Builder();
        int id = store.getMapId(name);
        MVMap map = store.getMap(id);
        if (map == null) {
            String configAsString = store.getMetaMap().get(MVMap.getMapKey(id));
            HashMap<String, Object> config;
            if (configAsString != null) {
                config = new HashMap<String, Object>(DataUtils.parseMap(configAsString));
            } else {
                config = new HashMap<>();
            }
            config.put("id", id);
            map = mapBuilder.create(store, config);
            long root = store.getRootPos(store.getMetaMap(), id);
            // 该方法执行的就是从文件中提取出 page 
            map.setRootPos(root, MVMap.INITIAL_VERSION);
            store.maps.put(id, map);
            // 提取出 page
            Page p = map.getRootPage();
            System.out.println(p);
        }

复制

Page 类是操作数据的核心类

输入 Page 和要查询的索引,通过 tree 来查询结果

/**
     * Get the value for the given key, or null if not found.
     * Search is done in the tree rooted at given page.
     *
     * @param key the key
     * @param p the root page
     * @return the value, or null if not found
     */
    public static Object get(Page p, Object key) {
        while (true) {
            int index = p.binarySearch(key);
            if (p.isLeaf()) {
                return index >= 0 ? p.getValue(index) : null;
            } else if (index++ < 0) {
                index = -index;
            }
            p = p.getChildPage(index);
        }
    }

复制

插入 key-value 到叶子节点

public void insertLeaf(int index, Object key, Object value) {
            int keyCount = getKeyCount();
            insertKey(index, key);
            if(values != null) {
                Object[] newValues = createValueStorage(keyCount + 1);
                DataUtils.copyWithGap(values, newValues, keyCount, index);
                values = newValues;
                setValueInternal(index, value);
                if (isPersistent()) {
                    addMemory(MEMORY_POINTER + map.getValueType().getMemory(value));
                }
            }
        }

复制

到这里,读者基本上就了解了 h2 的存储内核了,这个还是比较简单,容易掌握和扩展的。如果大家对存储内核有兴趣的话,可以加入 DawnSql 交流群,告诉我,我会继续写下去。DawnSql 交流群,在 https://docs.dawnsql.com/ 的首页可以查看(打开有点慢,稍等一下就可以了)

说明一点:有些朋友有疑问,为什么 DawnSql 选择 h2 的存储内核,而不是去重新做一个?这里主要是为了高用性!h2 作为成熟的数据库存储内核,已经在实际的项目中应用了多年,它是经得起考验的。如果新做存储内核,可能会给使用者带来高可用性上面的顾虑,所以我们再三权衡后选择更稳定可用性更高的方案。当然随着 DawnSql 的发展和根据企业方的要求,我们也可以对其进行修改和重构!

相关文章
|
10月前
|
存储 物联网 调度
操作系统的心脏:内核深度解析
在数字世界的构建中,操作系统扮演着基石的角色,而其核心—内核,则是这一复杂系统的灵魂。本文将深入探讨操作系统内核的工作原理,揭示它是如何管理硬件资源、运行程序以及提供系统服务的。通过理解内核的结构和功能,我们可以更好地把握计算机系统的运作机制,进而优化和创新我们的技术实践。
|
8月前
|
存储 人工智能 NoSQL
Tablestore深度解析:面向AI场景的结构化数据存储最佳实践
《Tablestore深度解析:面向AI场景的结构化数据存储最佳实践》由阿里云专家团队分享,涵盖Tablestore十年发展历程、AI时代多模态数据存储需求、VCU模式优化、向量检索发布及客户最佳实践等内容。Tablestore支持大规模在线数据存储,提供高性价比、高性能和高可用性,特别针对AI场景进行优化,满足结构化与非结构化数据的统一存储和高效检索需求。通过多元化索引和Serverless弹性VCU模式,助力企业实现低成本、灵活扩展的数据管理方案。
382 12
|
8月前
|
存储 分布式计算 Hadoop
基于Java的Hadoop文件处理系统:高效分布式数据解析与存储
本文介绍了如何借鉴Hadoop的设计思想,使用Java实现其核心功能MapReduce,解决海量数据处理问题。通过类比图书馆管理系统,详细解释了Hadoop的两大组件:HDFS(分布式文件系统)和MapReduce(分布式计算模型)。具体实现了单词统计任务,并扩展支持CSV和JSON格式的数据解析。为了提升性能,引入了Combiner减少中间数据传输,以及自定义Partitioner解决数据倾斜问题。最后总结了Hadoop在大数据处理中的重要性,鼓励Java开发者学习Hadoop以拓展技术边界。
262 7
|
10月前
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
390 24
|
9月前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
10月前
|
存储 安全 数据安全/隐私保护
PyPI 存储库中的 JarkaStealer:深入解析与防范措施
PyPI 存储库中的 JarkaStealer:深入解析与防范措施
107 2
|
10月前
|
存储 人工智能 安全
操作系统的心脏——内核深度解析
【10月更文挑战第29天】 本文深入探讨了操作系统的核心组件——内核,包括其定义、功能、架构以及在现代计算中的重要性。通过对比不同操作系统内核的设计哲学和技术实现,揭示了内核如何影响系统性能、稳定性和安全性。此外,文章还讨论了未来内核技术的潜在发展方向,为读者提供了一个全面了解内核工作原理的平台。
|
10月前
|
存储 消息中间件 算法
深入探索操作系统的心脏——内核机制解析
本文旨在揭示操作系统核心——内核的工作原理,通过剖析其关键组件与机制,为读者提供一个清晰的内核结构图景。不同于常规摘要的概述性内容,本文摘要将直接聚焦于内核的核心概念、主要功能以及其在系统管理中扮演的角色,旨在激发读者对操作系统深层次运作原理的兴趣与理解。
|
10月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
185 4
|
10月前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####

推荐镜像

更多
  • DNS