当前位置: 首页 > news >正文

从零手写Java版本的LSM Tree (七):压缩策略

🔥 推荐一个高质量的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支持开源!

第7章:压缩策略

什么是压缩?

压缩 (Compaction) 是LSM Tree的核心机制,负责合并多个SSTable文件以:

  • 减少文件数量: 避免查询时需要检查太多文件
  • 清理冗余数据: 删除过期版本和墓碑标记
  • 优化空间利用: 提高存储效率
  • 维护有序性: 保持数据在磁盘上的有序排列

压缩触发条件

压缩触发场景:
1. SSTable文件数量超过阈值
2. 某层文件大小超过限制
3. 手动触发压缩
4. 定期压缩任务
5. 读放大过大时

压缩策略类型

1. Size-Tiered 压缩策略

我们采用Size-Tiered压缩策略,它是一种基于文件数量触发的分层压缩机制:

核心思想

  • 将SSTable文件按层级(Level)组织
  • 每层允许的最大文件数量有限制
  • 当某层文件数量达到阈值时,将该层所有文件合并到下一层
  • 层级越高,文件越大,数量越少

具体工作流程

层级最大文件数单文件大小总容量触发条件
Level 04个文件~10MB~40MB4个文件时触发
Level 14个文件~40MB~160MB4个文件时触发
Level 24个文件~160MB~640MB4个文件时触发
Level N4个文件4^N × 10MB递增4个文件时触发

压缩触发示例

步骤1: MemTable刷盘产生SSTable文件
Level 0: [file1.sst] [file2.sst] [file3.sst] [file4.sst] ← 达到4个文件步骤2: Level 0达到阈值,触发压缩到Level 1  
Level 0: [] (清空)
Level 1: [merged_L1_1.sst] ← 4个文件合并成1个大文件步骤3: 继续写入,Level 0再次累积
Level 0: [file5.sst] [file6.sst] [file7.sst] [file8.sst] ← 又达到4个
Level 1: [merged_L1_1.sst] [merged_L1_2.sst] ← 新压缩的文件步骤4: Level 1达到阈值,触发向Level 2压缩
Level 1: [] (清空)
Level 2: [merged_L2_1.sst] ← 更大的合并文件

优势分析

  • 写入友好: Level 0直接接收MemTable,写入延迟低
  • 空间效率: 定期清理过期数据和删除标记
  • 读取优化: 减少需要查询的文件数量
  • ⚠️ 写放大: 数据可能被多次压缩(但比Leveled更少)

压缩策略实现

核心实现

package com.brianxiadong.lsmtree;import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;public class CompactionStrategy {private final String dataDirectory;              // 数据存储目录路径private final int maxFilesPerLevel;              // 每层允许的最大文件数量private final Map<Integer, List<String>> levelFiles; // 层级到文件列表的映射// 压缩策略构造器public CompactionStrategy(String dataDirectory, int maxFilesPerLevel) {this.dataDirectory = dataDirectory;          // 设置数据目录this.maxFilesPerLevel = maxFilesPerLevel;    // 设置每层文件数量阈值// 使用ConcurrentHashMap确保多线程安全访问层级文件映射this.levelFiles = new ConcurrentHashMap<>();}// 将新的SSTable文件添加到Level 0(MemTable刷盘后调用)public void addSSTable(String filePath) {// computeIfAbsent确保Level 0的文件列表存在,然后添加新文件levelFiles.computeIfAbsent(0, k -> new ArrayList<>()).add(filePath);}// 检查指定层级是否需要压缩public boolean needsCompaction(int level) {List<String> files = levelFiles.get(level);  // 获取该层的文件列表// 当文件数量达到或超过阈值时需要压缩return files != null && files.size() >= maxFilesPerLevel;}// 执行指定层级的压缩操作public void compact(int level) throws IOException {if (!needsCompaction(level)) {               // 检查是否真的需要压缩return;                                  // 不需要压缩,直接返回}// 获取当前层所有需要压缩的文件(创建副本避免并发修改)List<String> filesToCompact = new ArrayList<>(levelFiles.get(level));// 执行实际的文件合并操作,生成下一层的文件String compactedFile = performCompaction(filesToCompact, level + 1);// 更新文件层级结构levelFiles.get(level).clear();              // 清空当前层(文件已合并)// 将合并后的文件添加到下一层levelFiles.computeIfAbsent(level + 1, k -> new ArrayList<>()).add(compactedFile);// 物理删除旧的SSTable文件,释放磁盘空间cleanupOldFiles(filesToCompact);// 递归检查下一层是否也需要压缩(压缩可能引发连锁反应)if (needsCompaction(level + 1)) {compact(level + 1);                     // 递归压缩下一层}}
}

核心设计解析:这个压缩策略管理器是Size-Tiered压缩的控制中心。它维护了一个层级到文件列表的映射,使用ConcurrentHashMap确保多线程安全。addSSTable方法将新文件添加到Level 0,这是MemTable刷盘的入口点。needsCompaction方法简单但关键,它决定了压缩的触发时机。compact方法实现了压缩的完整流程:检查→合并→更新→清理→递归检查,这种设计确保了压缩操作的原子性和连锁效应的正确处理。

压缩执行器

// 执行实际的文件压缩合并操作
private String performCompaction(List<String> inputFiles, int targetLevel) throws IOException {// 打印压缩开始信息,用于监控和调试System.out.printf("开始压缩 %d 个文件到 Level %d%n", inputFiles.size(), targetLevel);// 步骤1: 加载所有需要压缩的输入SSTable文件List<SSTable> inputTables = new ArrayList<>();for (String filePath : inputFiles) {// 从磁盘文件加载SSTable对象到内存inputTables.add(SSTable.loadFromFile(filePath));  }// 步骤2: 使用多路归并算法合并所有SSTable的数据List<KeyValue> mergedData = mergeSSTableData(inputTables);// 步骤3: 数据去重和清理(移除过期版本、处理删除标记)List<KeyValue> cleanedData = deduplicateAndClean(mergedData);// 步骤4: 为目标层级生成新的SSTable文件名String outputFile = generateCompactedFileName(targetLevel);// 步骤5: 将清理后的数据写入新的SSTable文件SSTable compactedTable = new SSTable(outputFile, cleanedData);// 打印压缩完成信息,显示压缩效果统计System.out.printf("压缩完成: %s (清理前: %d条, 清理后: %d条)%n", outputFile, mergedData.size(), cleanedData.size());return outputFile;  // 返回新生成的SSTable文件路径
}

压缩执行解析:这个方法是压缩操作的核心执行引擎,它将多个SSTable文件合并成一个更大的文件。整个过程分为5个清晰的步骤:文件加载、数据合并、数据清理、文件命名、文件写入。数据合并使用多路归并算法确保输出数据的有序性,数据清理阶段移除冗余版本和处理删除操作,这是压缩提高存储效率的关键环节。统计信息的输出帮助监控压缩效果,了解数据清理的程度。

数据合并算法

// 使用多路归并算法合并多个有序SSTable的数据
private List<KeyValue> mergeSSTableData(List<SSTable> tables) {// 创建最小堆,用于多路归并排序(按key字典序排序)PriorityQueue<SSTableIterator> heap = new PriorityQueue<>(Comparator.comparing(iter -> iter.current().getKey())  // 比较器:按当前key排序);// 初始化所有SSTable的迭代器,将第一个元素放入堆for (SSTable table : tables) {SSTableIterator iter = table.iterator();    // 获取表的迭代器if (iter.hasNext()) {                      // 如果表非空iter.next();                           // 移动到第一个元素heap.offer(iter);                      // 将迭代器放入堆}}List<KeyValue> merged = new ArrayList<>();     // 存储合并后的有序数据// 执行多路归并:每次取出最小key的迭代器while (!heap.isEmpty()) {SSTableIterator iter = heap.poll();        // 取出堆顶(最小key)的迭代器KeyValue current = iter.current();         // 获取当前最小的KeyValuemerged.add(current);                       // 添加到结果中// 如果该迭代器还有数据,继续放入堆中if (iter.hasNext()) {iter.next();                           // 移动到下一个元素heap.offer(iter);                      // 重新放入堆中排序}}return merged;  // 返回合并排序后的所有数据
}// SSTable迭代器:用于遍历单个SSTable的数据
private static class SSTableIterator {private final List<KeyValue> data;    // SSTable中的所有数据private int index = -1;               // 当前位置索引(-1表示未开始)// 构造器:从SSTable获取所有数据public SSTableIterator(SSTable table) {this.data = table.getAllData();   // 加载SSTable的所有KeyValue数据}// 检查是否还有下一个元素public boolean hasNext() {return index + 1 < data.size();   // 判断下一个位置是否有效}// 移动到下一个元素并返回public KeyValue next() {if (!hasNext()) {                 // 边界检查throw new NoSuchElementException("没有更多元素");}return data.get(++index);         // 先移动索引,再返回元素}// 获取当前元素(不移动索引)public KeyValue current() {if (index < 0 || index >= data.size()) {  // 检查索引有效性return null;                           // 无效位置返回null}return data.get(index);                    // 返回当前位置的元素}
}

多路归并解析:这是压缩算法的核心,它将多个有序的SSTable合并成一个大的有序序列。算法使用最小堆维护各个SSTable的"当前最小元素",每次取出全局最小的key,确保输出序列的有序性。时间复杂度为O(N log K),其中N是总元素数,K是SSTable数量。这种设计既保证了合并效率,又维持了LSM Tree要求的数据有序性。迭代器模式使得内存使用可控,即使处理大文件也不会出现内存溢出。

去重和清理

// 对已排序的数据进行去重和清理,移除过期版本和删除标记
private List<KeyValue> deduplicateAndClean(List<KeyValue> sortedData) {if (sortedData.isEmpty()) {               // 空数据直接返回return sortedData;}List<KeyValue> cleaned = new ArrayList<>();  // 存储清理后的数据String lastKey = null;                       // 上一个处理的keyKeyValue lastKV = null;                      // 上一个key的最新版本// 遍历所有已排序的KeyValue(按key排序,相同key按timestamp排序)for (KeyValue kv : sortedData) {String currentKey = kv.getKey();         // 获取当前记录的keyif (!currentKey.equals(lastKey)) {       // 遇到新的key// 处理上一个key:添加其最新版本(如果未被删除)if (lastKV != null && !lastKV.isDeleted()) {cleaned.add(lastKV);             // 只保留未删除的记录}// 开始处理新keylastKey = currentKey;                // 更新当前处理的keylastKV = kv;                        // 设置当前版本为候选最新版本} else {// 相同key的多个版本:保留时间戳最新的版本if (kv.getTimestamp() > lastKV.getTimestamp()) {lastKV = kv;                    // 更新为更新的版本}// 旧版本被丢弃,实现去重}}// 处理最后一个key:添加其最新版本(如果未被删除)if (lastKV != null && !lastKV.isDeleted()) {cleaned.add(lastKV);                    // 确保最后一个key也被处理}return cleaned;  // 返回去重和清理后的数据
}

去重清理解析:这是压缩过程中数据优化的关键步骤,它实现了LSM Tree的两个重要功能:版本去重和删除处理。对于同一个key的多个版本,只保留时间戳最新的版本,这大大减少了存储空间。对于标记为删除的记录,在压缩时彻底移除,释放存储空间。算法的时间复杂度为O(N),空间复杂度也是O(N),效率很高。这种设计确保了压缩后的数据既保持了最新状态,又最大化了存储效率。

小结

压缩策略是LSM Tree性能的关键:

  1. 文件管理: 控制文件数量和大小
  2. 空间回收: 清理冗余和删除的数据
  3. 性能平衡: 在读写性能间找到平衡
  4. 自适应: 根据负载模式调整策略

思考题

  1. 为什么需要多层压缩?
  2. 如何选择合适的压缩触发条件?
  3. 压缩过程中如何保证数据一致性?

下一章预告: 我们将学习如何将所有组件整合成完整的LSM Tree系统。

相关文章:

  • 第二章:文本处理与表示的基础 —— 解码语言的奥秘
  • 运放——单电源供电和双电源供电
  • PyG测试GCN无线通信网络拓扑推理方法时间复杂度
  • Windows11+VS2019配置Libigl-2.4.1
  • Green Gold Label 绿金标认证
  • el-switch文字内置
  • 【Linux指南】用户与系统基础操作
  • 华为OD机试-最短木板长度-二分法(A卷,100分)
  • 【时时三省】(C语言基础)变量的存储方式和生存期
  • 医美宏脉软件如何通过快解析实现远程访问?
  • 【java】【服务器】线程上下文丢失 是指什么
  • 【51单片机】4. 模块化编程与LCD1602Debug
  • 2025最全TS手写题之partial/Omit/Pick/Exclude/Readonly/Required
  • STM32使用水位传感器
  • 牛客round95D
  • 科伦药业:以“三发引擎”驱动创新,全面迈入价值收获新周期
  • TCA 循环中间体如何改写肝损伤命运【AbMole】
  • VAS1086Q 奇力科技线性芯片车规用品LED驱动芯片
  • 无人机EN 18031欧盟网络安全认证详细解读
  • ABAP设计模式之---“童子军法则(The Boy Scout Rule)”
  • 与动漫有关的专业/优化的含义
  • 网站建设方案多少钱/app软件开发
  • 电子商务网站推广方法和技巧/百度关键词指数
  • 制作动态网站/电商运营多少钱一个月
  • 做网站后台维护的岗位叫什么/网络营销专业是干嘛的
  • 广州 网站建设/网页设计主要做什么