Files
srdb/CLAUDE.md
bourdon 7d2bb4745c 重构:清理项目结构和完善文档
- 添加完整的 DOCS.md 文档(1376 行)
- 更新 README.md,增强项目说明
- 清理临时示例和测试数据
- 删除诊断工具(已完成测试)
- 为 webui 示例准备测试数据
- 优化 .gitignore 配置
- 增强 Query 和 Schema 功能
- 改进 SSTable 编码处理
2025-10-10 18:36:22 +08:00

437 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
SRDB 是一个用 Go 编写的高性能 Append-Only 时序数据库引擎。它使用简化的 LSM-tree 架构,结合 WAL + MemTable + mmap B+Tree SST 文件针对高并发写入200K+ 写/秒和快速查询1-5ms进行了优化。
**模块**: `code.tczkiot.com/wlw/srdb`
## 构建和测试
```bash
# 运行所有测试
go test -v ./...
# 运行单个测试
go test -v -run TestSSTable
go test -v -run TestTable
# 运行性能测试
go test -bench=. -benchmem
# 运行带超时的测试(某些 compaction 测试需要较长时间)
go test -v -timeout 30s
# 构建 WebUI 工具
cd examples/webui
go build -o webui main.go
./webui serve --db ./data
```
## 架构
### 文件结构(扁平化设计)
所有核心代码都在根目录下,采用扁平化结构:
```
srdb/
├── database.go # 多表数据库管理
├── table.go # 表管理(带 Schema
├── errors.go # 错误定义和处理(统一错误码系统)
├── wal.go # WAL 实现Write-Ahead Log
├── memtable.go # MemTablemap + sorted slice~130 行)
├── sstable.go # SSTable 文件(读写器、管理器、二进制编码)
├── btree.go # B+Tree 索引构建器、读取器4KB 节点)
├── version.go # 版本控制MANIFEST 管理)
├── compaction.go # Compaction 压缩合并
├── schema.go # Schema 定义与验证
├── index.go # 二级索引管理器
├── index_btree.go # 索引 B+Tree 实现
└── query.go # 查询构建器和表达式求值
```
**运行时数据目录**:
```
database_dir/
├── database.meta # 数据库元数据JSON
├── MANIFEST # 全局版本控制
└── table_name/ # 每表一个目录
├── schema.json # 表 Schema 定义
├── MANIFEST # 表级版本控制
├── 000001.wal # WAL 文件
├── 000001.sst # SST 文件B+Tree 索引 + 二进制数据)
└── idx_field.sst # 二级索引文件(可选)
```
### 核心架构:简化的两层模型
与传统的多层 LSM 树不同SRDB 使用简化的两层架构:
1. **内存层**: WAL + MemTable (Active + Immutable)
2. **磁盘层**: 带 B+Tree 索引的 SST 文件,分为 L0-L4+ 层级
### 核心数据流
**写入路径**:
1. Schema 验证(强制要求,如果表有 Schema
2. 生成序列号 (`_seq`,原子递增的 int64
3. 追加写入 WAL顺序写
4. 插入到 Active MemTablemap + 有序 slice
5. 当 MemTable 超过阈值(默认 64MB切换到新的 Active MemTable 并异步刷新 Immutable 到 SST
6. 更新二级索引(如果字段标记为 Indexed
**读取路径**:
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)
- 权衡:插入新 key 时需要重新排序 keys slice但实际上仍然更快
- Active MemTable + 多个 Immutable MemTables正在刷新中
**SST 格式: 4KB 节点的 B+Tree**
- 固定大小的节点,与 OS 页面大小对齐
- 支持高效的 mmap 访问和零拷贝读取
- 内部节点keys + 子节点指针
- 叶子节点keys + 数据偏移量/大小
- 数据块:二进制编码(使用 Schema 时)或 JSON无 Schema 时)
**二进制编码格式**:
- Magic Number: `0x524F5731` ("ROW1")
- 格式:`[Magic: 4B][Seq: 8B][Time: 8B][FieldCount: 2B][FieldOffsetTable][FieldData]`
- 按字段分别编码,支持部分字段读取(`GetPartial`
- 无压缩(优先查询性能,保持 mmap 零拷贝)
**mmap 而非 read() 系统调用**
- 对 SST 文件的零拷贝访问
- OS 自动管理页面缓存
- 应用程序内存占用 < 150MB无论数据大小
**Append-only无更新/删除)**
- 简化并发控制
- 相同 seq 的新记录覆盖旧记录
- Compaction 合并文件并按 seq 去重保留最新的按时间戳
## 常见开发模式
### Schema 系统(强制要求)
从最近的重构开始Schema **强制**不再支持无 Schema 模式
```go
schema := NewSchema("users", []Field{
{Name: "name", Type: String, Indexed: false, Comment: "用户名"},
{Name: "age", Type: Int64, Indexed: false, Comment: "年龄"},
{Name: "email", Type: String, Indexed: true, Comment: "邮箱(索引)"},
})
table, _ := db.CreateTable("users", schema)
```
- Schema `Insert()` 时强制验证类型和必填字段
- 索引字段`Indexed: true`自动创建二级索引
- Schema 持久化到 `table_dir/schema.json`包含校验和防篡改
- **支持的类型** (21 精确映射到 Go 基础类型):
- **有符号整数** (5种): `Int`, `Int8`, `Int16`, `Int32`, `Int64`
- **无符号整数** (5种): `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
- **浮点数** (2种): `Float32`, `Float64`
- **字符串** (1种): `String`
- **布尔** (1种): `Bool`
- **特殊类型** (5种): `Byte` (独立类型底层=uint8), `Rune` (独立类型底层=int32), `Decimal` (高精度十进制使用 shopspring/decimal), `Time` (time.Time), `Duration` (time.Duration)
- **复杂类型** (2种): `Object` (map[string]xxxstruct{}、*struct{}使用 JSON 编码), `Array` ([]xxx 切片使用 JSON 编码)
- **Nullable 支持**: 字段可标记为 `Nullable: true`允许 NULL
### 类型系统详解
**精确类型映射**:
v1.x 开始SRDB 采用精确类型映射策略每个 Go 基础类型都有对应的 FieldType这带来以下优势
1. **存储优化**: 使用 `uint8` (1 字节) 存储百分比而不是 `int64` (8 字节)
2. **语义明确**: `uint32` 表示设备ID`float32` 表示传感器读数
3. **类型安全**: 编译期和运行期双重类型检查
**类型转换规则**:
```go
// 1. 相同类型:直接接受
{Name: "age", Type: Int32}
Insert(map[string]any{"age": int32(25)}) // ✓
// 2. 兼容类型:自动转换(有符号 ↔ 无符号,需非负)
{Name: "count", Type: Int64}
Insert(map[string]any{"count": uint32(100)}) // ✓
// 3. 类型提升:整数 → 浮点
{Name: "ratio", Type: Float32}
Insert(map[string]any{"ratio": int32(42)}) // ✓ 转为 42.0
// 4. JSON 兼容float64 → 整数(需为整数值)
{Name: "id", Type: Int64}
Insert(map[string]any{"id": float64(123.0)}) // ✓ JSON 反序列化场景
// 5. 负数 → 无符号:拒绝
{Name: "index", Type: Uint32}
Insert(map[string]any{"index": int32(-1)}) // ✗ 错误
```
**最佳实践**:
```go
// 推荐:根据数据范围选择合适的类型
schema, _ := NewSchema("sensors", []Field{
{Name: "device_id", Type: Uint32}, // 0 ~ 42亿
{Name: "temperature", Type: Float32}, // 单精度足够
{Name: "humidity", Type: Uint8}, // 0-100
{Name: "status", Type: Bool}, // 布尔状态
})
// 避免:盲目使用 int64 和 float64
schema, _ := NewSchema("sensors_bad", []Field{
{Name: "device_id", Type: Int64}, // 浪费 4 字节
{Name: "temperature", Type: Float64}, // 浪费 4 字节
{Name: "humidity", Type: Int64}, // 浪费 7 字节!
{Name: "status", Type: Int64}, // 浪费 7 字节!
})
```
**新增类型的使用场景**:
```go
// Byte 类型 - 状态码、标志位
{Name: "status_code", Type: Byte, Comment: "HTTP 状态码 (0-255)"}
Insert(map[string]any{"status_code": uint8(200)}) // byte 和 uint8 底层相同
// Rune 类型 - 单个字符
{Name: "grade", Type: Rune, Comment: "等级 (S/A/B/C)"}
Insert(map[string]any{"grade": rune('A')}) // rune 和 int32 底层相同
// Decimal 类型 - 金融计算
{Name: "amount", Type: Decimal, Comment: "交易金额"}
import "github.com/shopspring/decimal"
Insert(map[string]any{"amount": decimal.NewFromFloat(123.456)})
// Nullable 支持 - 可选字段
{Name: "email", Type: String, Nullable: true, Comment: "邮箱(可选)"}
Insert(map[string]any{"email": nil}) // 允许 NULL
Insert(map[string]any{"email": "user@example.com"}) // 或有值
```
**从结构体自动生成 Schema**:
```go
type Sensor struct {
DeviceID uint32 `srdb:"device_id;indexed;comment:设备ID"`
Temperature float32 `srdb:"temperature;comment:温度"`
Humidity uint8 `srdb:"humidity;comment:湿度 0-100"`
Online bool `srdb:"online;comment:是否在线"`
}
// 自动映射:
// uint32 → Uint32
// float32 → Float32
// uint8 → Uint8 (也可用 byte)
// bool → Bool
fields, _ := StructToFields(Sensor{})
schema, _ := NewSchema("sensors", fields)
```
**参考示例**:
- `examples/all_types/` - 展示所有 17 种类型的基本使用
- `examples/new_types/` - 展示新增的 ByteRuneDecimal 类型和 Nullable 支持的实际应用场景
### Query Builder
对于带条件的查询使用链式 API
```go
// 简单查询
rows, _ := table.Query().Eq("name", "Alice").Rows()
// 复合条件
rows, _ := table.Query().
Eq("status", "active").
Gte("age", 18).
Rows()
// 字段选择(性能优化)
rows, _ := table.Query().
Select("id", "name", "email").
Eq("status", "active").
Rows()
// 游标模式
rows, _ := table.Query().Rows()
defer rows.Close()
for rows.Next() {
row := rows.Row()
fmt.Println(row.Data())
}
```
支持的操作符`Eq`, `NotEq`, `Lt`, `Gt`, `Lte`, `Gte`, `In`, `NotIn`, `Between`, `Contains`, `StartsWith`, `EndsWith`, `IsNull`, `NotNull`
### Compaction
Compaction 在后台自动运行
- **触发条件**: L0 文件数 > 阈值(默认 4-10根据层级
- **策略**: 合并重叠文件,从 L0 → L1、L1 → L2 等
- **Score 计算**: `size / max_size``file_count / max_files`
- **安全性**: 删除前验证文件是否存在,以防止数据丢失
- **去重**: 对于重复的 seq保留最新记录按时间戳
- **文件大小**: L0=2MB、L1=10MB、L2=50MB、L3=100MB、L4+=200MB
修改 compaction 逻辑时,注意 `compaction.go` 中的文件选择和合并逻辑。
### 版本控制MANIFEST
MANIFEST 跟踪跨版本的 SST 文件元数据:
- `VersionEdit`: 记录原子变更AddFile/DeleteFile
- `VersionSet`: 管理当前和历史版本
- `LogAndApply()`: 原子地应用编辑并持久化到 MANIFEST
添加/删除 SST 文件时:
1. 分配文件编号:`versionSet.AllocateFileNumber()`
2. 创建带变更的 `VersionEdit`
3. 应用:`versionSet.LogAndApply(edit)`
4. 清理旧文件(通过 GC 机制)
### 错误处理
使用统一的错误码系统(`errors.go`
```go
// 创建错误
err := NewError(ErrCodeTableNotFound, nil)
// 带上下文包装错误
err := WrapError(baseErr, "failed to get table %s", "users")
// 错误判断
if IsNotFound(err) { ... }
if IsCorrupted(err) { ... }
if IsClosed(err) { ... }
// 获取错误码
code := GetErrorCode(err)
```
- 错误码范围1000-1999通用、2000-2999数据库、3000-3999、4000-4999Schema
- 所有 panic 已替换为错误返回
- 使用 `fmt.Errorf``%w` 进行错误链包装
### 错误恢复
- **WAL 重放**: 启动时,所有 `*.wal` 文件被重放到 Active MemTable
- **孤儿文件清理**: 不在 MANIFEST 中的文件在启动时删除(有年龄保护,避免误删最近写入的文件)
- **索引修复**: 自动验证和重建损坏的索引
- **优雅降级**: 表恢复失败会被记录但不会使数据库崩溃
## 重要实现细节
### 序列号系统
- `_seq` 是单调递增的 int64原子操作
- 充当主键和时间戳排序
- 永不重用append-only
- Compaction 期间,相同 seq 值的较新记录优先(按 `_time` 排序)
### 并发控制
- `Table.mu`: 保护表级元数据
- `SSTableManager.mu`: RWMutex保护 SST reader 列表
- `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 (4KB nodes) | Data Blocks (Binary format)
```
**B+Tree 节点**4KB 固定):
```
Header (32B) | Keys (8B each) | Pointers/Offsets (8B each) | Padding
```
**二进制行格式** (ROW1):
```
Magic (4B) | Seq (8B) | Time (8B) | FieldCount (2B) |
[FieldOffset, FieldSize] × N | FieldData × N
```
## 性能特性
- **写入吞吐量**: 200K+ 写/秒多线程50K 写/秒(单线程)
- **写入延迟**: < 1msp99
- **查询延迟**: < 0.1msMemTable1-5msSST 热数据3-5ms冷数据
- **内存使用**: < 150MB64MB MemTable + 开销
- **压缩**: 未使用优先查询性能
优化建议
- 批量写入以减少 WAL 同步开销
- 对经常查询的字段创建索引
- 使用 `Select()` 只查询需要的字段
- 监控 MemTable 刷新频率不应太频繁
- 根据写入模式调整 Compaction 阈值
## 常见陷阱
- **Schema 是强制的**: 所有表必须定义 Schema不再支持无 Schema 模式
- **索引非自动创建**: 需要在 Schema 中显式标记 `Indexed: true`
- **类型名称简化**:
- **重要变更**: v2.0 开始类型名称已简化使用简短形式 `String` 而非 `FieldTypeString`
- 每个 Go 类型有对应的简短常量 `int32` `Int32``string` `String`
- 插入时类型会自动转换 `int` `int32`但需要注意负数不能转为无符号类型
- **新增类型的使用**:
- **Byte**: 虽然底层是 `uint8`但在 Schema 中作为独立类型语义更清晰用于状态码标志位等
- **Rune**: 虽然底层是 `int32`但在 Schema 中作为独立类型用于存储单个 Unicode 字符
- **Decimal**: 必须使用 `github.com/shopspring/decimal` 用于金融计算等需要精确数值的场景
- **Nullable 支持**:
- 需要显式标记 `Nullable: true`默认字段不允许 NULL
- NULL 值在 Go 中表示为 `nil`
- 读取时需要检查值是否存在且不为 nil
- **选择合适的类型大小**:
- 避免盲目使用 `Int64`/`Float64`根据数据范围选择如百分比用 `Uint8`状态码用 `Byte`
- 过大的类型浪费存储和内存影响性能
- **Compaction 磁盘占用**: 合并期间旧文件和新文件共存会暂时增加磁盘使用
- **MemTable flush 异步**: 关闭时需要等待 immutable flush 完成
- **mmap 虚拟内存**: 可能显示较大的虚拟内存使用正常OS 管理不是实际 RAM
- ** panic**: 所有 panic 已替换为错误返回需要正确处理错误
## Web UI
项目包含功能完善的 Web 管理界面
```bash
cd examples/webui
go run main.go serve --db /path/to/database --port 8080
```
功能
- 表管理和数据浏览
- Manifest 可视化LSM-Tree 结构
- 实时 Compaction 监控
- 深色/浅色主题
详见 `examples/webui/README.md`