从零手写Java版本的LSM Tree (四):SSTable 磁盘存储

🔥 推荐一个高质量的Java LSM Tree开源项目!
https://github.com/brianxiadong/java-lsm-tree
java-lsm-tree 是一个从零实现的Log-Structured Merge Tree,专为高并发写入场景设计。
核心亮点:
⚡ 极致性能:写入速度超过40万ops/秒,完爆传统B+树
🏗️ 完整架构:MemTable跳表 + SSTable + WAL + 布隆过滤器 + 多级压缩
📚 深度教程:12章详细教程,从基础概念到生产优化,每行代码都有注释
🔒 并发安全:读写锁机制,支持高并发场景
💾 数据可靠:WAL写前日志确保崩溃恢复,零数据丢失
适合谁?

  • 想深入理解LSM Tree原理的开发者
  • 需要高写入性能存储引擎的项目
  • 准备数据库/存储系统面试的同学
  • 对分布式存储感兴趣的工程师
    ⭐ 给个Star支持开源!

第4章:SSTable 磁盘存储

什么是SSTable?

SSTable (Sorted String Table) 是LSM Tree中存储在磁盘上的不可变有序文件。当MemTable达到大小阈值时,会将其内容刷盘生成SSTable文件。

SSTable 核心特性

1. 不可变性 (Immutability)

  • 一旦写入完成,SSTable文件永不修改
  • 更新操作通过新的SSTable体现
  • 删除操作通过墓碑标记实现

2. 有序性 (Sorted)

  • 所有键值对key的字典序排列
  • 支持高效的二分查找
  • 便于合并操作

3. 自包含性 (Self-contained)

  • 包含布隆过滤器用于快速过滤
  • 包含索引信息加速查找
  • 包含元数据信息

关于布隆过滤器,大家先简单认为用来判断数据存在不存在即可,下一小节会详细进行讲解。

文件格式设计

我们的SSTable采用简化的文件格式:

┌─────────────────────────────────────────────────────────────┐
│                    SSTable 文件结构                         │
├─────────────────────────────────────────────────────────────┤
│  [条目数量: 4字节]                                           │
├─────────────────────────────────────────────────────────────┤
│  [数据条目区域]                                             │
│    条目1: key|value|timestamp|deleted                       │
│    条目2: key|value|timestamp|deleted                       │
│    ...                                                      │
│    条目N: key|value|timestamp|deleted                       │
├─────────────────────────────────────────────────────────────┤
│  [布隆过滤器区域]                                           │
│    过滤器数据                                               │
└─────────────────────────────────────────────────────────────┘

SSTable 实现解析

让我们深入分析SSTable的实现:

package com.brianxiadong.lsmtree;import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;/*** Sorted String Table (SSTable) 实现* 磁盘上的有序不可变文件*/
public class SSTable {// 文件路径:SSTable存储在磁盘上的位置private final String filePath;// 布隆过滤器:用于快速判断键是否可能存在private final BloomFilter bloomFilter;// 创建时间:用于压缩时的文件排序private final long creationTime;// 构造函数:从有序数据创建SSTablepublic SSTable(String filePath, List<KeyValue> sortedData) throws IOException {this.filePath = filePath;                           // 设置文件路径this.creationTime = System.currentTimeMillis();    // 记录创建时间// 创建布隆过滤器,估算条目数和假阳性率this.bloomFilter = new BloomFilter(sortedData.size(), 0.01);// 将数据持久化到磁盘文件writeToFile(sortedData);}/*** 从文件路径加载已存在的SSTable*/public SSTable(String filePath) throws IOException {this.filePath = filePath;                                           // 设置文件路径this.creationTime = Files.getLastModifiedTime(Paths.get(filePath)) // 获取文件修改时间作为创建时间.toMillis();this.bloomFilter = new BloomFilter(1000, 0.01);                    // 创建临时布隆过滤器// 重新构建布隆过滤器rebuildBloomFilter();}
}

代码解释: 这个SSTable类是LSM Tree持久化存储的核心。它包含三个关键组件:文件路径用于磁盘存储,布隆过滤器用于快速过滤不存在的键,有序数据列表用于内存中的快速访问。构造函数会自动创建布隆过滤器并将数据写入磁盘。

核心方法分析

1. 写入文件 (writeToFile)
/*** 将排序数据写入文件*/
private void writeToFile(List<KeyValue> sortedData) throws IOException {// 使用DataOutputStream进行二进制写入,性能更好try (DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filePath)))) {// 写入条目数量dos.writeInt(sortedData.size());                    // 写入整数类型的条目数// 写入所有数据条目for (KeyValue kv : sortedData) {// 添加到布隆过滤器bloomFilter.add(kv.getKey());                   // 添加键到过滤器// 写入数据:key, deleted, value(如果不是删除), timestampdos.writeUTF(kv.getKey());                      // 写入键(UTF-8编码)dos.writeBoolean(kv.isDeleted());               // 写入删除标记if (!kv.isDeleted()) {                          // 如果不是删除操作dos.writeUTF(kv.getValue());                // 写入值}dos.writeLong(kv.getTimestamp());               // 写入时间戳}}// try-with-resources自动关闭DataOutputStream
}

代码解释: 写入过程采用顺序写入策略,这是磁盘I/O的最优模式。文件格式简单明了:首先是条目数量,然后是所有数据条目,最后是布隆过滤器。使用管道符(|)分隔字段,便于解析。BufferedWriter提供缓冲以提高写入性能。

写入过程分析:

  1. 顺序写入: 充分利用磁盘顺序写入的高性能
  2. 文本格式: 便于调试和人工检查
  3. 完整性: 包含所有必要的元数据
2. 重建布隆过滤器 (rebuildBloomFilter)
/*** 重新构建布隆过滤器*/
private void rebuildBloomFilter() throws IOException {// 使用DataInputStream读取二进制文件try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(filePath)))) {int totalEntries = dis.readInt();                   // 读取条目总数// 遍历所有条目,将键添加到布隆过滤器for (int i = 0; i < totalEntries; i++) {String key = dis.readUTF();                     // 读取键boolean deleted = dis.readBoolean();            // 读取删除标记if (!deleted) {                                 // 如果不是删除操作dis.readUTF();                              // 跳过value,只读取键用于布隆过滤器}dis.readLong();                                 // 跳过timestamp// 添加到布隆过滤器bloomFilter.add(key);                           // 将键添加到过滤器}}// try-with-resources自动关闭DataInputStream
}

代码解释: 加载过程是写入的逆向操作。先读取条目数量以便预估内存需求,然后逐行解析数据条目,最后恢复布隆过滤器。如果布隆过滤器数据损坏,会自动重建,增强了系统的容错性。解析使用split方法按管道符分割字段。

3. 查询操作 (get)
/*** 查询键值 - 简化实现,顺序搜索*/
public String get(String key) {// 首先检查布隆过滤器if (!bloomFilter.mightContain(key)) {return null;                                        // 布隆过滤器说不存在,直接返回null}// 布隆过滤器说可能存在,读取文件进行查找try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(filePath)))) {int totalEntries = dis.readInt();                   // 读取条目总数// 顺序搜索所有条目for (int i = 0; i < totalEntries; i++) {String currentKey = dis.readUTF();              // 读取当前键boolean deleted = dis.readBoolean();            // 读取删除标记String value = null;                            // 初始化值if (!deleted) {                                 // 如果不是删除操作value = dis.readUTF();                      // 读取值}long timestamp = dis.readLong();                // 读取时间戳// 检查是否找到目标键if (currentKey.equals(key)) {return deleted ? null : value;              // 如果是删除标记返回null,否则返回值}// 由于数据有序,如果当前键大于目标键,则不存在if (currentKey.compareTo(key) > 0) {break;                                      // 提前退出循环}}} catch (IOException e) {e.printStackTrace();                                // 打印异常信息}return null;                                            // 未找到,返回null
}

代码解释: 查询操作采用两阶段策略:首先用布隆过滤器快速过滤不存在的键,这能避免大部分无效的磁盘访问。如果布隆过滤器表示键可能存在,再进行文件扫描。由于数据有序存储,当遇到比目标键大的键时可以提前退出。这种实现虽然是顺序查找,但在实际应用中由于布隆过滤器的过滤效果,大多数查询都不需要访问磁盘。

4. 获取所有条目 (getAllEntries)
/*** 获取所有键值对(用于合并)*/
public List<KeyValue> getAllEntries() throws IOException {List<KeyValue> entries = new ArrayList<>();            // 创建结果列表// 读取文件中的所有数据try (DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(filePath)))) {int totalEntries = dis.readInt();                   // 读取条目总数// 读取所有条目for (int i = 0; i < totalEntries; i++) {String key = dis.readUTF();                     // 读取键boolean deleted = dis.readBoolean();            // 读取删除标记String value = null;                            // 初始化值if (!deleted) {                                 // 如果不是删除操作value = dis.readUTF();                      // 读取值}long timestamp = dis.readLong();                // 读取时间戳// 创建KeyValue对象并添加到列表entries.add(new KeyValue(key, value, timestamp, deleted));}}return entries;                                         // 返回所有条目
}/*** 删除SSTable文件*/
public void delete() throws IOException {Files.deleteIfExists(Paths.get(filePath));              // 删除文件,如果存在的话
}// Getter方法
public String getFilePath() {return filePath;                                        // 返回文件路径
}public long getCreationTime() {return creationTime;                                    // 返回创建时间
}

代码解释: getAllEntries方法用于压缩过程中读取SSTable的所有数据。它按顺序读取文件中的所有条目,重建KeyValue对象。delete方法提供文件清理功能,在压缩完成后删除旧的SSTable文件。Getter方法提供对文件路径和创建时间的访问,用于压缩策略中的文件排序。

小结

SSTable是LSM Tree的持久化存储层,具有以下关键特性:

  1. 不可变性: 确保数据一致性和线程安全
  2. 有序性: 支持高效查找和范围查询
  3. 自包含: 包含布隆过滤器和索引信息
  4. 优化: 多种性能优化技术

思考题

  1. 为什么SSTable要设计为不可变的?
  2. 布隆过滤器如何提升SSTable的查询性能?
  3. 如何在保持性能的同时减少SSTable的磁盘占用?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/85052.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kotlin的5个主要作用域函数

applay, also,let, run, with 是kotlin标准库提供的5个主要的作用域函数&#xff08;Scope Functions&#xff09;​&#xff0c;它们的设计目的是为了在特定作用域内更简洁地操作对象。 如何使用这5个函数&#xff0c;要从它的设计目的来区分&#xff1a; apply : 配置/对象…

原型模式Prototype Pattern

模式定义 用原型实例指定创建对象的种类&#xff0c;并且通过复制这些原型创建新的对象&#xff0c;其允许一个对象再创建 另外一个可定制的对象&#xff0c;无须知道任何创建的细节 对象创建型模式 基本工作原理是通过将一个原型对象传给那个要发动创建的对象&#xff0c;这…

基于深度学习的智能交通流量预测系统:技术与实践

前言 随着城市化进程的加速&#xff0c;交通拥堵问题日益严重&#xff0c;给人们的日常生活和经济发展带来了巨大的挑战。智能交通系统&#xff08;ITS&#xff09;作为解决交通问题的重要手段&#xff0c;逐渐成为研究的热点。其中&#xff0c;交通流量预测是智能交通系统中的…

Cilium动手实验室: 精通之旅---23.Advanced Gateway API Use Cases

Cilium动手实验室: 精通之旅---23.Advanced Gateway API Use Cases 1. Lab说明1.1 高级网关 API 使用案例 2. 负载均衡器2.1 部署应用程序2.2 部署 Gateway 和 HTTPRoute 3. HTTP 标头请求修饰符3.1 部署 HTTPRoute3.2 可观测性 4. HTTP 响应标头重写5. HTTP 流量镜像5.1 demo应…

Agentic Workflow是什么?Agentic Workflow会成为下一个AI风口吗?

无论是想要学习人工智能当做主业营收&#xff0c;还是像我一样作为开发工程师但依然要运用这个颠覆开发的时代宠儿&#xff0c;都有必要了解、学习一下人工智能。 近期发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;入行门槛低&#x…

Some chunks are larger than 500 KiB after minification. Consider

在 vue3vite 项目开发中&#xff0c;build 打包时出现以下警告报错&#xff1a; (!) Some chunks are larger than 500 KiB after minification. Consider: - Using dynamic import() to code-split the application - Use build.rollupOptions.output.manualChunks to improve…

NodeJS11和10以及之前的版本,关键差异?

Node.js 11 相比 10&#xff08;及更早版本&#xff09;&#xff0c;除了事件循环行为的重大改变&#xff0c;还有多个核心模块和底层机制的升级。以下是它们的关键差异和新特性对比&#xff0c;帮助你快速掌握两个版本的重要变化。 &#x1f527; 一、事件循环行为变化&#x…

调和级数 敛散性

调和级数的敛散性是一个非常经典的问题。我们来全面分析它。 &#x1f9e0; 调和级数定义 调和级数是指&#xff1a; ∑ n 1 ∞ 1 n 1 1 2 1 3 1 4 ⋯ \sum_{n1}^{\infty} \frac{1}{n} 1 \frac{1}{2} \frac{1}{3} \frac{1}{4} \cdots n1∑∞​n1​121​31​41​⋯ …

Python•元组集合字符串

ʕ⸝⸝⸝˙Ⱉ˙ʔ ♡ 元组&#x1f6e5;️创建访问修改解包其他操作比较的依据 集合&#x1f6f8;创建添加和删除其他操作 字符串&#x1fa82;创建索引和切片基本操作连接加号join() 重复查找in 关键字index()find()startswith()endswith() ​​替换​​分割​​大小写删除 能…

​​信息系统项目管理师-项目整合管理 知识点总结与例题分析​​

​​一、项目整合管理概述​​ ​​1. 定义与重要性​​ 项目整合管理是项目管理知识领域中的核心过程,它协调所有其他知识领域的过程和活动,确保项目各要素有效整合。其核心目标是: ​​统一项目目标​​:确保各要素服务于共同目标​​协调冲突​​:解决项目执行中的各…

『uniapp』onThemeChange监听主题样式,动态主题不正确生效,样式被覆盖的坑

目录 问题示例代码解决思路1&#xff08;缺点影响显示效果有延迟&#xff09;解决思路2——通过路由刷新页面&#xff08;缺点只适用于部分网页&#xff09;解决思路3——vuex&#xff08;没学会~&#xff09;总结 欢迎关注 『uniapp』 专栏&#xff0c;持续更新中 欢迎关注 『…

LeetCode 高频 SQL 50 题(基础版)【题解】合集

点击下方标题可跳转至对应部分&#xff1a; LeetCode 高频 SQL 50 题&#xff08;基础版&#xff09;之 【查询】部分 LeetCode 高频 SQL 50 题&#xff08;基础版&#xff09;之 【连接】部分 上 LeetCode 高频 SQL 50 题&#xff08;基础版&#xff09;之 【连接】部分 下…

Jenkins 全面深入学习目录

Jenkins 全面深入学习目录 第一部分&#xff1a;Jenkins 基础入门 Jenkins 概述 持续集成/持续交付(CI/CD)概念Jenkins 的历史与发展Jenkins 与其他 CI/CD 工具的比较 Jenkins 安装与配置 系统要求与环境准备不同操作系统下的安装方法初始配置与安全设置插件管理系统 Jenkins…

安装laravel11和laravel12的一些报错问题解决

前言 今天在安装laravel的过程中遇到一些报错问题&#xff0c;记录一下。 laravel 12 Root composer.json requires laravel/tinker ^2.10.1, found laravel/tinker[2.x-dev] but it does not match your minimum-stability laravel/framework[v12.0.0, ..., v12.15.0] requ…

Oracle21cR3之客户端安装错误及处理方法

文章目录 Oracle21cR3客户端安装1. 下载2. 安装解压到指定位置&#xff0c;如下&#xff1a;2. 安装 3. 常见错误1. 无法将 JINSHENGYUAN\jinshengyuan 安装用户添加到 %2% 组。1. 问题原因分析2. 处理方法 Oracle21cR3客户端安装 1. 下载 官网下载 2. 安装 解压到指定位置…

web3 资讯网址

1. 新闻 币圈导航| 区块链导航| WEB3导航 | 聚合币圈交易所、行情工具、空投资讯、DeFi入口及行业动态&#xff0c;一站式区块链资源门户网站 2.github位置 https://github.com/itgoyo/awesome-crypto

【C++】简单商品价格计算程序练习

相信你是最棒哒!!! 文章目录 一、题目代码 二、题目解析 1.解析版 2.简洁版 总结 一、题目代码 构建一个类book,其中含有两个私有数据成员qu和price,将price初始化为qu的10倍,建立一个有5个元素的数组对象,将qu初始化为6~10。要求通过对象指针访问对象数组,按相反的顺序…

现代数据工程实践:基于Dagster的ETL架构设计与实现

在当今数据驱动的世界中&#xff0c;有效的数据处理流程至关重要。本文将带您通过一个完整的教程&#xff0c;学习如何使用Dagster构建一个功能强大的ETL(提取、转换、加载)管道。无论您是数据工程师、分析师还是对数据流水线感兴趣的技术爱好者&#xff0c;本教程都将为您提供…

golang-linux环境配置

下载源码包 &#xff1a;All releases - The Go Programming Language 解压文件 sudo tar -zxvf go1.24.4.linux-amd64.tar.gz -C /usr/local/ 配置环境 vim ~/.bashrc 在配置文件最后加上下面三行&#xff1a; # 设置GO语言的路径 export GOROOT/usr/local/go # 当前go…

【模拟 贪心】B4207 [常州市赛 2021] 战士|普及+

B4207 [常州市赛 2021] 战士 题目背景 搬运自 http://czoj.com.cn/p/443。数据为民间数据。 题目描述 小 X \text X X 在玩一款操控战士和怪物战斗的游戏。战士初始生命值为 iH \text{iH} iH 、初始攻击力为 iA \text{iA} iA 。怪物只有一个&#xff0c;初始生命值为 H…