Files
srdb/CLAUDE.md
bourdon ae87c38776 Initial commit: SRDB - High-performance LSM-Tree database
- Core engine with MemTable, SST, WAL
- B+Tree indexing for SST files  
- Leveled compaction strategy
- Multi-table database management
- Schema validation and secondary indexes
- Query builder with complex conditions
- Web UI with HTMX for data visualization
- Command-line tools for diagnostics
2025-10-08 06:38:28 +08:00

10 KiB
Raw Blame History

CLAUDE.md

本文件为 Claude Code (claude.ai/code) 提供在本仓库中工作的指导。

项目概述

SRDB 是一个用 Go 编写的高性能 Append-Only 时序数据库引擎。它使用简化的 LSM-tree 架构,结合 WAL + MemTable + mmap B+Tree SST 文件针对高并发写入200K+ 写/秒和快速查询1-5ms进行了优化。

模块: code.tczkiot.com/srdb

构建和测试

# 运行所有测试
go test -v ./...

# 运行指定包的测试
go test -v ./engine
go test -v ./compaction
go test -v ./query

# 运行指定的测试
go test -v ./engine -run TestEngineBasic

# 构建示例程序
go build ./examples/basic
go build ./examples/with_schema

架构

两层存储模型

与传统的多层 LSM 树不同SRDB 使用简化的两层架构:

  1. 内存层: WAL + MemTable (Active + Immutable)
  2. 磁盘层: 带 B+Tree 索引的 SST 文件,分为 L0-L4+ 层级

核心数据流

写入路径:

  1. Schema 验证(如果定义了)
  2. 生成序列号 (_seq)
  3. 追加写入 WAL顺序写
  4. 插入到 Active MemTablemap + 有序 slice
  5. 当 MemTable 超过阈值(默认 64MB切换到新的 Active MemTable 并异步将 Immutable 刷新到 SST
  6. 更新二级索引(如果已创建)

读取路径:

  1. 检查 Active MemTableO(1) map 查找)
  2. 按顺序检查 Immutable MemTables从最新到最旧
  3. 使用 mmap + B+Tree 索引扫描 SST 文件(从最新到最旧)
  4. 第一个匹配的记录获胜(新数据覆盖旧数据)

查询路径(带条件):

  1. 如果是带 = 操作符的索引字段:使用二级索引 → 通过 seq 获取
  2. 否则带过滤条件的全表扫描MemTable + SST

关键设计选择

MemTable: map[int64][]byte + sorted []int64

  • 为什么不用 SkipList实现更简单130 行Put 和 Get 都是 O(1) vs O(log N)
  • 权衡:插入时需要重新排序 keys slice但实际上仍然更快
  • Active MemTable + 多个 Immutable MemTables正在刷新中

SST 格式: 4KB 节点的 B+Tree

  • 固定大小的节点,与 OS 页面大小对齐
  • 支持高效的 mmap 访问和零拷贝读取
  • 内部节点keys + 子节点指针
  • 叶子节点keys + 数据偏移量/大小
  • 数据块Snappy 压缩的 JSON 行

mmap 而非 read() 系统调用

  • 对 SST 文件的零拷贝访问
  • OS 自动管理页面缓存
  • 应用程序内存占用 < 150MB无论数据大小

Append-only无更新/删除)

  • 简化并发控制
  • 相同 seq 的新记录覆盖旧记录
  • Compaction 合并文件并按 seq 去重(保留最新的,按时间戳)

目录结构

srdb/
├── database.go           # 多表数据库管理
├── table.go              # 带 schema 的表
├── engine/               # 核心存储引擎583 行)
│   └── engine.go
├── wal/                  # 预写日志
│   ├── wal.go           # WAL 实现208 行)
│   └── manager.go       # 多 WAL 管理
├── memtable/            # 内存表
│   ├── memtable.go      # MemTable130 行)
│   └── manager.go       # Active + Immutable 管理
├── sst/                 # SSTable 文件
│   ├── format.go        # 文件格式定义
│   ├── writer.go        # SST 写入器
│   ├── reader.go        # mmap 读取器147 行)
│   ├── manager.go       # SST 文件管理
│   └── encoding.go      # Snappy 压缩
├── btree/               # B+Tree 索引
│   ├── node.go          # 4KB 节点结构
│   ├── builder.go       # B+Tree 构建器125 行)
│   └── reader.go        # B+Tree 读取器
├── manifest/            # 版本控制
│   ├── version_set.go   # 版本管理
│   ├── version_edit.go  # 原子更新
│   ├── version.go       # 文件元数据
│   ├── manifest_writer.go
│   └── manifest_reader.go
├── compaction/          # 后台压缩
│   ├── manager.go       # Compaction 调度器
│   ├── compactor.go     # 合并执行器
│   └── picker.go        # 文件选择策略
├── index/               # 二级索引
│   ├── index.go         # 字段级索引
│   └── manager.go       # 索引生命周期
├── query/               # 查询系统
│   ├── builder.go       # 流式查询 API
│   └── expr.go          # 表达式求值
└── schema/              # Schema 验证
    ├── schema.go        # 类型定义和验证
    └── examples.go      # Schema 示例

运行时数据目录(例如 ./mydb/:

database_dir/
├── database.meta        # 数据库元数据JSON
├── MANIFEST             # 全局版本控制
└── table_name/          # 每表目录
    ├── schema.json      # 表 schema
    ├── MANIFEST         # 表级版本控制
    ├── wal/             # WAL 文件(*.wal
    ├── sst/             # SST 文件(*.sst
    └── index/           # 二级索引idx_*.sst

常见模式

使用 Engine

Engine 是核心存储层。修改引擎行为时:

  • 所有写入都经过 Insert() → WAL → MemTable → (异步刷新到 SST
  • 读取经过 Get(seq) → 检查 MemTable → 检查 SST 文件
  • switchMemTable() 创建新的 Active MemTable 并异步刷新旧的
  • flushImmutable() 将 MemTable 写入 SST 并更新 MANIFEST
  • 后台 compaction 通过 compactionManager 运行

Schema 和验证

Schema 是可选的,但建议在生产环境使用:

schema := schema.NewSchema("users").
    AddField("name", schema.FieldTypeString, false, "用户名").
    AddField("age", schema.FieldTypeInt64, false, "用户年龄").
    AddField("email", schema.FieldTypeString, true, "邮箱(索引)")

table, _ := db.CreateTable("users", schema)
  • Schema 在 Insert() 时验证类型和必填字段
  • 索引字段(Indexed: true)自动创建二级索引
  • Schema 持久化到 table_dir/schema.json

Query Builder

对于带条件的查询,始终使用 QueryBuilder

qb := query.NewQueryBuilder()
qb.Where("age", query.OpGreater, 18).
   Where("city", query.OpEqual, "Beijing")
rows, _ := table.Query(qb)
  • 支持操作符:OpEqualOpNotEqualOpGreaterOpLessOpPrefixOpSuffixOpContains
  • 支持 WhereNot() 进行否定
  • 支持 And()Or() 逻辑
  • 当可用时自动使用二级索引(对于 = 条件)
  • 如果没有索引,则回退到全表扫描

Compaction

Compaction 在后台自动运行:

  • 触发条件: L0 文件数 > 阈值(默认 10
  • 策略: 合并重叠文件,从 L0 → L1、L1 → L2 等
  • 安全性: 删除前验证文件是否存在,以防止数据丢失
  • 去重: 对于重复的 seq保留最新记录按时间戳
  • 文件大小: L0=2MB、L1=10MB、L2=50MB、L3=100MB、L4+=200MB

修改 compaction 逻辑时:

  • picker.go: 选择要压缩的文件
  • compactor.go: 执行合并操作
  • manager.go: 调度和协调 compaction
  • 删除前始终验证输入/输出文件是否存在(参见 DoCompaction

版本控制MANIFEST

MANIFEST 跟踪跨版本的 SST 文件元数据:

  • VersionEdit: 记录原子变更AddFile/DeleteFile
  • VersionSet: 管理当前和历史版本
  • LogAndApply(): 原子地应用编辑并持久化到 MANIFEST

添加/删除 SST 文件时:

  1. 分配文件编号:versionSet.AllocateFileNumber()
  2. 创建带变更的 VersionEdit
  3. 应用:versionSet.LogAndApply(edit)
  4. 清理旧文件:compactionManager.CleanupOrphanFiles()

错误恢复

  • WAL 重放: 启动时,所有 *.wal 文件被重放到 Active MemTable
  • 孤儿文件清理: 不在 MANIFEST 中的文件在启动时删除
  • 索引修复: verifyAndRepairIndexes() 重建损坏的索引
  • 优雅降级: 表恢复失败会被记录但不会使数据库崩溃

测试模式

测试按组件组织:

  • engine/engine_test.go: 基本引擎操作
  • engine/engine_compaction_test.go: Compaction 场景
  • engine/engine_stress_test.go: 并发压力测试
  • compaction/compaction_test.go: Compaction 正确性
  • query/builder_test.go: Query builder 功能
  • schema/schema_test.go: Schema 验证

为多线程操作编写测试时,使用 sync.WaitGroup 并用多个 goroutine 测试(参见 engine_stress_test.go)。

性能特性

  • 写入吞吐量: 200K+ 写/秒多线程50K 写/秒(单线程)
  • 写入延迟: < 1msp99
  • 查询延迟: < 0.1msMemTable1-5msSST 热数据3-5ms冷数据
  • 内存使用: < 150MB64MB MemTable + 开销)
  • 压缩率: Snappy 约 50%

优化时:

  • 批量写入以减少 WAL 同步开销
  • 对经常查询的字段创建索引
  • 监控 MemTable 刷新频率(不应太频繁)
  • 根据写入模式调整 compaction 阈值

重要实现细节

序列号

  • _seq 是单调递增的 int64原子操作
  • 充当主键和时间戳排序
  • 永不重用append-only
  • compaction 期间,相同 seq 值的较新记录优先

并发

  • Engine.mu: 保护元数据和 SST reader 列表
  • Engine.flushMu: 确保一次只有一个 flush
  • MemTable.mu: RWMutex支持并发读、独占写
  • VersionSet.mu: 保护版本状态

文件格式

WAL 条目:

CRC32 (4B) | Length (4B) | Type (1B) | Seq (8B) | DataLen (4B) | Data (N bytes)

SST 文件:

Header (256B) | B+Tree Index | Data Blocks (Snappy compressed)

B+Tree 节点4KB 固定):

Header (32B) | Keys (8B each) | Pointers/Offsets (8B each) | Padding

常见陷阱

  • Schema 验证仅在向 Engine.Open() 提供 schema 时才应用
  • 索引必须通过 CreateIndex(field) 显式创建(非自动)
  • 带 schema 的 QueryBuilder 需要调用 WithSchema() 或让引擎设置它
  • Compaction 可能会暂时增加磁盘使用(合并期间旧文件和新文件共存)
  • MemTable flush 是异步的;关闭时可能需要等待 immutable flush 完成
  • mmap 文件可能显示较大的虚拟内存使用(这是正常的,不是实际 RAM