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
This commit is contained in:
2025-10-08 06:38:12 +08:00
commit ae87c38776
61 changed files with 15475 additions and 0 deletions

98
sst/encoding.go Normal file
View File

@@ -0,0 +1,98 @@
package sst
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
)
// 二进制编码格式:
// [Magic: 4 bytes][Seq: 8 bytes][Time: 8 bytes][DataLen: 4 bytes][Data: variable]
const (
RowMagic = 0x524F5733 // "ROW3"
)
// encodeRowBinary 使用二进制格式编码行数据
func encodeRowBinary(row *Row) ([]byte, error) {
buf := new(bytes.Buffer)
// 写入 Magic Number (用于验证)
if err := binary.Write(buf, binary.LittleEndian, uint32(RowMagic)); err != nil {
return nil, err
}
// 写入 Seq
if err := binary.Write(buf, binary.LittleEndian, row.Seq); err != nil {
return nil, err
}
// 写入 Time
if err := binary.Write(buf, binary.LittleEndian, row.Time); err != nil {
return nil, err
}
// 序列化用户数据 (仍使用 JSON但只序列化用户数据部分)
dataBytes, err := json.Marshal(row.Data)
if err != nil {
return nil, err
}
// 写入数据长度
if err := binary.Write(buf, binary.LittleEndian, uint32(len(dataBytes))); err != nil {
return nil, err
}
// 写入数据
if _, err := buf.Write(dataBytes); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// decodeRowBinary 解码二进制格式的行数据
func decodeRowBinary(data []byte) (*Row, error) {
buf := bytes.NewReader(data)
// 读取并验证 Magic Number
var magic uint32
if err := binary.Read(buf, binary.LittleEndian, &magic); err != nil {
return nil, err
}
if magic != RowMagic {
return nil, fmt.Errorf("invalid row magic: %x", magic)
}
row := &Row{}
// 读取 Seq
if err := binary.Read(buf, binary.LittleEndian, &row.Seq); err != nil {
return nil, err
}
// 读取 Time
if err := binary.Read(buf, binary.LittleEndian, &row.Time); err != nil {
return nil, err
}
// 读取数据长度
var dataLen uint32
if err := binary.Read(buf, binary.LittleEndian, &dataLen); err != nil {
return nil, err
}
// 读取数据
dataBytes := make([]byte, dataLen)
if _, err := buf.Read(dataBytes); err != nil {
return nil, err
}
// 反序列化用户数据
if err := json.Unmarshal(dataBytes, &row.Data); err != nil {
return nil, err
}
return row, nil
}

117
sst/encoding_test.go Normal file
View File

@@ -0,0 +1,117 @@
package sst
import (
"encoding/json"
"testing"
)
func TestBinaryEncoding(t *testing.T) {
// 创建测试数据
row := &Row{
Seq: 12345,
Time: 1234567890,
Data: map[string]interface{}{
"name": "test_user",
"age": 25,
"email": "test@example.com",
},
}
// 编码
encoded, err := encodeRowBinary(row)
if err != nil {
t.Fatal(err)
}
t.Logf("Encoded size: %d bytes", len(encoded))
// 解码
decoded, err := decodeRowBinary(encoded)
if err != nil {
t.Fatal(err)
}
// 验证
if decoded.Seq != row.Seq {
t.Errorf("Seq mismatch: expected %d, got %d", row.Seq, decoded.Seq)
}
if decoded.Time != row.Time {
t.Errorf("Time mismatch: expected %d, got %d", row.Time, decoded.Time)
}
if decoded.Data["name"] != row.Data["name"] {
t.Errorf("Name mismatch")
}
t.Log("Binary encoding test passed!")
}
func TestEncodingComparison(t *testing.T) {
row := &Row{
Seq: 12345,
Time: 1234567890,
Data: map[string]interface{}{
"name": "test_user",
"age": 25,
"email": "test@example.com",
},
}
// 二进制编码
binaryEncoded, _ := encodeRowBinary(row)
// JSON 编码 (旧方式)
jsonData := map[string]interface{}{
"_seq": row.Seq,
"_time": row.Time,
"data": row.Data,
}
jsonEncoded, _ := json.Marshal(jsonData)
t.Logf("Binary size: %d bytes", len(binaryEncoded))
t.Logf("JSON size: %d bytes", len(jsonEncoded))
t.Logf("Space saved: %.1f%%", float64(len(jsonEncoded)-len(binaryEncoded))/float64(len(jsonEncoded))*100)
if len(binaryEncoded) >= len(jsonEncoded) {
t.Error("Binary encoding should be smaller than JSON")
}
}
func BenchmarkBinaryEncoding(b *testing.B) {
row := &Row{
Seq: 12345,
Time: 1234567890,
Data: map[string]interface{}{
"name": "test_user",
"age": 25,
"email": "test@example.com",
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
encodeRowBinary(row)
}
}
func BenchmarkJSONEncoding(b *testing.B) {
row := &Row{
Seq: 12345,
Time: 1234567890,
Data: map[string]interface{}{
"name": "test_user",
"age": 25,
"email": "test@example.com",
},
}
data := map[string]interface{}{
"_seq": row.Seq,
"_time": row.Time,
"data": row.Data,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(data)
}
}

142
sst/format.go Normal file
View File

@@ -0,0 +1,142 @@
package sst
import (
"encoding/binary"
)
const (
// 文件格式
MagicNumber = 0x53535433 // "SST3"
Version = 1
HeaderSize = 256 // 文件头大小
BlockSize = 64 * 1024 // 数据块大小 (64 KB)
// 压缩类型
CompressionNone = 0
CompressionSnappy = 1
)
// Header SST 文件头 (256 bytes)
type Header struct {
// 基础信息 (32 bytes)
Magic uint32 // Magic Number: 0x53535433
Version uint32 // 版本号
Compression uint8 // 压缩类型
Reserved1 [3]byte
Flags uint32 // 标志位
Reserved2 [16]byte
// 索引信息 (32 bytes)
IndexOffset int64 // B+Tree 索引起始位置
IndexSize int64 // B+Tree 索引大小
RootOffset int64 // B+Tree 根节点位置
Reserved3 [8]byte
// 数据信息 (32 bytes)
DataOffset int64 // 数据块起始位置
DataSize int64 // 数据块总大小
RowCount int64 // 行数
Reserved4 [8]byte
// 统计信息 (32 bytes)
MinKey int64 // 最小 key (_seq)
MaxKey int64 // 最大 key (_seq)
MinTime int64 // 最小时间戳
MaxTime int64 // 最大时间戳
// CRC 校验 (8 bytes)
CRC32 uint32 // Header CRC32
Reserved5 [4]byte
// 预留空间 (120 bytes)
Reserved6 [120]byte
}
// Marshal 序列化 Header
func (h *Header) Marshal() []byte {
buf := make([]byte, HeaderSize)
// 基础信息
binary.LittleEndian.PutUint32(buf[0:4], h.Magic)
binary.LittleEndian.PutUint32(buf[4:8], h.Version)
buf[8] = h.Compression
copy(buf[9:12], h.Reserved1[:])
binary.LittleEndian.PutUint32(buf[12:16], h.Flags)
copy(buf[16:32], h.Reserved2[:])
// 索引信息
binary.LittleEndian.PutUint64(buf[32:40], uint64(h.IndexOffset))
binary.LittleEndian.PutUint64(buf[40:48], uint64(h.IndexSize))
binary.LittleEndian.PutUint64(buf[48:56], uint64(h.RootOffset))
copy(buf[56:64], h.Reserved3[:])
// 数据信息
binary.LittleEndian.PutUint64(buf[64:72], uint64(h.DataOffset))
binary.LittleEndian.PutUint64(buf[72:80], uint64(h.DataSize))
binary.LittleEndian.PutUint64(buf[80:88], uint64(h.RowCount))
copy(buf[88:96], h.Reserved4[:])
// 统计信息
binary.LittleEndian.PutUint64(buf[96:104], uint64(h.MinKey))
binary.LittleEndian.PutUint64(buf[104:112], uint64(h.MaxKey))
binary.LittleEndian.PutUint64(buf[112:120], uint64(h.MinTime))
binary.LittleEndian.PutUint64(buf[120:128], uint64(h.MaxTime))
// CRC 校验
binary.LittleEndian.PutUint32(buf[128:132], h.CRC32)
copy(buf[132:136], h.Reserved5[:])
// 预留空间
copy(buf[136:256], h.Reserved6[:])
return buf
}
// Unmarshal 反序列化 Header
func UnmarshalHeader(data []byte) *Header {
if len(data) < HeaderSize {
return nil
}
h := &Header{}
// 基础信息
h.Magic = binary.LittleEndian.Uint32(data[0:4])
h.Version = binary.LittleEndian.Uint32(data[4:8])
h.Compression = data[8]
copy(h.Reserved1[:], data[9:12])
h.Flags = binary.LittleEndian.Uint32(data[12:16])
copy(h.Reserved2[:], data[16:32])
// 索引信息
h.IndexOffset = int64(binary.LittleEndian.Uint64(data[32:40]))
h.IndexSize = int64(binary.LittleEndian.Uint64(data[40:48]))
h.RootOffset = int64(binary.LittleEndian.Uint64(data[48:56]))
copy(h.Reserved3[:], data[56:64])
// 数据信息
h.DataOffset = int64(binary.LittleEndian.Uint64(data[64:72]))
h.DataSize = int64(binary.LittleEndian.Uint64(data[72:80]))
h.RowCount = int64(binary.LittleEndian.Uint64(data[80:88]))
copy(h.Reserved4[:], data[88:96])
// 统计信息
h.MinKey = int64(binary.LittleEndian.Uint64(data[96:104]))
h.MaxKey = int64(binary.LittleEndian.Uint64(data[104:112]))
h.MinTime = int64(binary.LittleEndian.Uint64(data[112:120]))
h.MaxTime = int64(binary.LittleEndian.Uint64(data[120:128]))
// CRC 校验
h.CRC32 = binary.LittleEndian.Uint32(data[128:132])
copy(h.Reserved5[:], data[132:136])
// 预留空间
copy(h.Reserved6[:], data[136:256])
return h
}
// Validate 验证 Header
func (h *Header) Validate() bool {
return h.Magic == MagicNumber && h.Version == Version
}

284
sst/manager.go Normal file
View File

@@ -0,0 +1,284 @@
package sst
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
// Manager SST 文件管理器
type Manager struct {
dir string
readers []*Reader
mu sync.RWMutex
}
// NewManager 创建 SST 管理器
func NewManager(dir string) (*Manager, error) {
// 确保目录存在
err := os.MkdirAll(dir, 0755)
if err != nil {
return nil, err
}
mgr := &Manager{
dir: dir,
readers: make([]*Reader, 0),
}
// 恢复现有的 SST 文件
err = mgr.recover()
if err != nil {
return nil, err
}
return mgr, nil
}
// recover 恢复现有的 SST 文件
func (m *Manager) recover() error {
// 查找所有 SST 文件
files, err := filepath.Glob(filepath.Join(m.dir, "*.sst"))
if err != nil {
return err
}
for _, file := range files {
// 跳过索引文件
filename := filepath.Base(file)
if strings.HasPrefix(filename, "idx_") {
continue
}
// 打开 SST Reader
reader, err := NewReader(file)
if err != nil {
return err
}
m.readers = append(m.readers, reader)
}
return nil
}
// CreateSST 创建新的 SST 文件
// fileNumber: 文件编号(由 VersionSet 分配)
func (m *Manager) CreateSST(fileNumber int64, rows []*Row) (*Reader, error) {
return m.CreateSSTWithLevel(fileNumber, rows, 0) // 默认创建到 L0
}
// CreateSSTWithLevel 创建新的 SST 文件到指定层级
// fileNumber: 文件编号(由 VersionSet 分配)
func (m *Manager) CreateSSTWithLevel(fileNumber int64, rows []*Row, level int) (*Reader, error) {
m.mu.Lock()
defer m.mu.Unlock()
sstPath := filepath.Join(m.dir, fmt.Sprintf("%06d.sst", fileNumber))
// 创建文件
file, err := os.Create(sstPath)
if err != nil {
return nil, err
}
writer := NewWriter(file)
// 写入所有行
for _, row := range rows {
err = writer.Add(row)
if err != nil {
file.Close()
os.Remove(sstPath)
return nil, err
}
}
// 完成写入
err = writer.Finish()
if err != nil {
file.Close()
os.Remove(sstPath)
return nil, err
}
file.Close()
// 打开 SST Reader
reader, err := NewReader(sstPath)
if err != nil {
return nil, err
}
// 添加到 readers 列表
m.readers = append(m.readers, reader)
return reader, nil
}
// Get 从所有 SST 文件中查找数据
func (m *Manager) Get(seq int64) (*Row, error) {
m.mu.RLock()
defer m.mu.RUnlock()
// 从后往前查找(新的文件优先)
for i := len(m.readers) - 1; i >= 0; i-- {
reader := m.readers[i]
row, err := reader.Get(seq)
if err == nil {
return row, nil
}
}
return nil, fmt.Errorf("key not found: %d", seq)
}
// GetReaders 获取所有 Readers用于扫描
func (m *Manager) GetReaders() []*Reader {
m.mu.RLock()
defer m.mu.RUnlock()
// 返回副本
readers := make([]*Reader, len(m.readers))
copy(readers, m.readers)
return readers
}
// GetMaxSeq 获取所有 SST 中的最大 seq
func (m *Manager) GetMaxSeq() int64 {
m.mu.RLock()
defer m.mu.RUnlock()
maxSeq := int64(0)
for _, reader := range m.readers {
header := reader.GetHeader()
if header.MaxKey > maxSeq {
maxSeq = header.MaxKey
}
}
return maxSeq
}
// Count 获取 SST 文件数量
func (m *Manager) Count() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.readers)
}
// ListFiles 列出所有 SST 文件
func (m *Manager) ListFiles() []string {
m.mu.RLock()
defer m.mu.RUnlock()
files := make([]string, 0, len(m.readers))
for _, reader := range m.readers {
files = append(files, reader.path)
}
return files
}
// CompactionConfig Compaction 配置
// 已废弃:请使用 compaction 包中的 Manager
type CompactionConfig struct {
Threshold int // 触发阈值SST 文件数量)
BatchSize int // 每次合并的文件数量
}
// DefaultCompactionConfig 默认配置
// 已废弃:请使用 compaction 包中的 Manager
var DefaultCompactionConfig = CompactionConfig{
Threshold: 10,
BatchSize: 10,
}
// ShouldCompact 检查是否需要 Compaction
// 已废弃:请使用 compaction 包中的 Manager
func (m *Manager) ShouldCompact(config CompactionConfig) bool {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.readers) > config.Threshold
}
// Compact 执行 Compaction
// 已废弃:请使用 compaction 包中的 Manager
// 注意:此方法已不再维护,不应在新代码中使用
func (m *Manager) Compact(config CompactionConfig) error {
// 此方法已废弃,不再实现
return fmt.Errorf("Compact is deprecated, please use compaction.Manager")
}
// sortRows 按 seq 排序
func sortRows(rows []*Row) {
sort.Slice(rows, func(i, j int) bool {
return rows[i].Seq < rows[j].Seq
})
}
// Delete 删除指定的 SST 文件(预留接口)
func (m *Manager) Delete(fileNumber int64) error {
m.mu.Lock()
defer m.mu.Unlock()
sstPath := filepath.Join(m.dir, fmt.Sprintf("%06d.sst", fileNumber))
return os.Remove(sstPath)
}
// Close 关闭所有 SST Readers
func (m *Manager) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
for _, reader := range m.readers {
reader.Close()
}
m.readers = nil
return nil
}
// Stats 统计信息
type Stats struct {
FileCount int
TotalSize int64
MinSeq int64
MaxSeq int64
}
// GetStats 获取统计信息
func (m *Manager) GetStats() *Stats {
m.mu.RLock()
defer m.mu.RUnlock()
stats := &Stats{
FileCount: len(m.readers),
MinSeq: -1,
MaxSeq: -1,
}
for _, reader := range m.readers {
header := reader.GetHeader()
if stats.MinSeq == -1 || header.MinKey < stats.MinSeq {
stats.MinSeq = header.MinKey
}
if stats.MaxSeq == -1 || header.MaxKey > stats.MaxSeq {
stats.MaxSeq = header.MaxKey
}
// 获取文件大小
if stat, err := os.Stat(reader.path); err == nil {
stats.TotalSize += stat.Size()
}
}
return stats
}

152
sst/reader.go Normal file
View File

@@ -0,0 +1,152 @@
package sst
import (
"encoding/json"
"fmt"
"os"
"code.tczkiot.com/srdb/btree"
"github.com/edsrzf/mmap-go"
"github.com/golang/snappy"
)
// Reader SST 文件读取器
type Reader struct {
path string
file *os.File
mmap mmap.MMap
header *Header
btReader *btree.Reader
}
// NewReader 创建 SST 读取器
func NewReader(path string) (*Reader, error) {
// 1. 打开文件
file, err := os.Open(path)
if err != nil {
return nil, err
}
// 2. mmap 映射
mmapData, err := mmap.Map(file, mmap.RDONLY, 0)
if err != nil {
file.Close()
return nil, err
}
// 3. 读取 Header
if len(mmapData) < HeaderSize {
mmapData.Unmap()
file.Close()
return nil, fmt.Errorf("file too small")
}
header := UnmarshalHeader(mmapData[:HeaderSize])
if header == nil || !header.Validate() {
mmapData.Unmap()
file.Close()
return nil, fmt.Errorf("invalid header")
}
// 4. 创建 B+Tree Reader
btReader := btree.NewReader(mmapData, header.RootOffset)
return &Reader{
path: path,
file: file,
mmap: mmapData,
header: header,
btReader: btReader,
}, nil
}
// Get 查询一行数据
func (r *Reader) Get(key int64) (*Row, error) {
// 1. 检查范围
if key < r.header.MinKey || key > r.header.MaxKey {
return nil, fmt.Errorf("key out of range")
}
// 2. 在 B+Tree 中查找
dataOffset, dataSize, found := r.btReader.Get(key)
if !found {
return nil, fmt.Errorf("key not found")
}
// 3. 读取数据
if dataOffset+int64(dataSize) > int64(len(r.mmap)) {
return nil, fmt.Errorf("invalid data offset")
}
compressed := r.mmap[dataOffset : dataOffset+int64(dataSize)]
// 4. 解压缩
var data []byte
var err error
if r.header.Compression == CompressionSnappy {
data, err = snappy.Decode(nil, compressed)
if err != nil {
return nil, err
}
} else {
data = compressed
}
// 5. 反序列化
row, err := decodeRow(data)
if err != nil {
return nil, err
}
return row, nil
}
// GetHeader 获取文件头信息
func (r *Reader) GetHeader() *Header {
return r.header
}
// GetPath 获取文件路径
func (r *Reader) GetPath() string {
return r.path
}
// GetAllKeys 获取文件中所有的 key按顺序
func (r *Reader) GetAllKeys() []int64 {
return r.btReader.GetAllKeys()
}
// Close 关闭读取器
func (r *Reader) Close() error {
if r.mmap != nil {
r.mmap.Unmap()
}
if r.file != nil {
return r.file.Close()
}
return nil
}
// decodeRow 解码行数据
func decodeRow(data []byte) (*Row, error) {
// 尝试使用二进制格式解码
row, err := decodeRowBinary(data)
if err == nil {
return row, nil
}
// 降级到 JSON (兼容旧数据)
var decoded map[string]interface{}
err = json.Unmarshal(data, &decoded)
if err != nil {
return nil, err
}
row = &Row{
Seq: int64(decoded["_seq"].(float64)),
Time: int64(decoded["_time"].(float64)),
Data: decoded["data"].(map[string]interface{}),
}
return row, nil
}

183
sst/sst_test.go Normal file
View File

@@ -0,0 +1,183 @@
package sst
import (
"os"
"testing"
)
func TestSST(t *testing.T) {
// 1. 创建测试文件
file, err := os.Create("test.sst")
if err != nil {
t.Fatal(err)
}
defer os.Remove("test.sst")
// 2. 写入数据
writer := NewWriter(file)
// 添加 1000 行数据
for i := int64(1); i <= 1000; i++ {
row := &Row{
Seq: i,
Time: 1000000 + i,
Data: map[string]interface{}{
"name": "user_" + string(rune(i)),
"age": 20 + i%50,
},
}
err := writer.Add(row)
if err != nil {
t.Fatal(err)
}
}
// 完成写入
err = writer.Finish()
if err != nil {
t.Fatal(err)
}
file.Close()
t.Logf("Written 1000 rows")
// 3. 读取数据
reader, err := NewReader("test.sst")
if err != nil {
t.Fatal(err)
}
defer reader.Close()
// 验证 Header
header := reader.GetHeader()
if header.RowCount != 1000 {
t.Errorf("Expected 1000 rows, got %d", header.RowCount)
}
if header.MinKey != 1 {
t.Errorf("Expected MinKey=1, got %d", header.MinKey)
}
if header.MaxKey != 1000 {
t.Errorf("Expected MaxKey=1000, got %d", header.MaxKey)
}
t.Logf("Header: RowCount=%d, MinKey=%d, MaxKey=%d",
header.RowCount, header.MinKey, header.MaxKey)
// 4. 查询测试
for i := int64(1); i <= 1000; i++ {
row, err := reader.Get(i)
if err != nil {
t.Errorf("Failed to get key %d: %v", i, err)
continue
}
if row.Seq != i {
t.Errorf("Key %d: expected Seq=%d, got %d", i, i, row.Seq)
}
if row.Time != 1000000+i {
t.Errorf("Key %d: expected Time=%d, got %d", i, 1000000+i, row.Time)
}
}
// 测试不存在的 key
_, err = reader.Get(1001)
if err == nil {
t.Error("Key 1001 should not exist")
}
_, err = reader.Get(0)
if err == nil {
t.Error("Key 0 should not exist")
}
t.Log("All tests passed!")
}
func TestHeaderSerialization(t *testing.T) {
// 创建 Header
header := &Header{
Magic: MagicNumber,
Version: Version,
Compression: CompressionSnappy,
IndexOffset: 256,
IndexSize: 1024,
RootOffset: 512,
DataOffset: 2048,
DataSize: 10240,
RowCount: 100,
MinKey: 1,
MaxKey: 100,
MinTime: 1000000,
MaxTime: 1000100,
}
// 序列化
data := header.Marshal()
if len(data) != HeaderSize {
t.Errorf("Expected size %d, got %d", HeaderSize, len(data))
}
// 反序列化
header2 := UnmarshalHeader(data)
if header2 == nil {
t.Fatal("Unmarshal failed")
}
// 验证
if header2.Magic != header.Magic {
t.Error("Magic mismatch")
}
if header2.Version != header.Version {
t.Error("Version mismatch")
}
if header2.Compression != header.Compression {
t.Error("Compression mismatch")
}
if header2.RowCount != header.RowCount {
t.Error("RowCount mismatch")
}
if header2.MinKey != header.MinKey {
t.Error("MinKey mismatch")
}
if header2.MaxKey != header.MaxKey {
t.Error("MaxKey mismatch")
}
// 验证
if !header2.Validate() {
t.Error("Header validation failed")
}
t.Log("Header serialization test passed!")
}
func BenchmarkSSTGet(b *testing.B) {
// 创建测试文件
file, _ := os.Create("bench.sst")
defer os.Remove("bench.sst")
writer := NewWriter(file)
for i := int64(1); i <= 10000; i++ {
row := &Row{
Seq: i,
Time: 1000000 + i,
Data: map[string]interface{}{
"value": i,
},
}
writer.Add(row)
}
writer.Finish()
file.Close()
// 打开读取器
reader, _ := NewReader("bench.sst")
defer reader.Close()
// 性能测试
b.ResetTimer()
for i := 0; i < b.N; i++ {
key := int64(i%10000 + 1)
reader.Get(key)
}
}

155
sst/writer.go Normal file
View File

@@ -0,0 +1,155 @@
package sst
import (
"encoding/json"
"os"
"code.tczkiot.com/srdb/btree"
"github.com/golang/snappy"
)
// Writer SST 文件写入器
type Writer struct {
file *os.File
builder *btree.Builder
dataOffset int64
dataStart int64 // 数据起始位置
rowCount int64
minKey int64
maxKey int64
minTime int64
maxTime int64
compression uint8
}
// NewWriter 创建 SST 写入器
func NewWriter(file *os.File) *Writer {
return &Writer{
file: file,
builder: btree.NewBuilder(file, HeaderSize),
dataOffset: 0, // 先写数据,后面会更新
compression: CompressionSnappy,
minKey: -1,
maxKey: -1,
minTime: -1,
maxTime: -1,
}
}
// Row 表示一行数据
type Row struct {
Seq int64 // _seq
Time int64 // _time
Data map[string]any // 用户数据
}
// Add 添加一行数据
func (w *Writer) Add(row *Row) error {
// 更新统计信息
if w.minKey == -1 || row.Seq < w.minKey {
w.minKey = row.Seq
}
if w.maxKey == -1 || row.Seq > w.maxKey {
w.maxKey = row.Seq
}
if w.minTime == -1 || row.Time < w.minTime {
w.minTime = row.Time
}
if w.maxTime == -1 || row.Time > w.maxTime {
w.maxTime = row.Time
}
w.rowCount++
// 序列化数据 (简单的 JSON 序列化,后续可以优化)
data := encodeRow(row)
// 压缩数据
var compressed []byte
if w.compression == CompressionSnappy {
compressed = snappy.Encode(nil, data)
} else {
compressed = data
}
// 写入数据块
// 第一次写入时,确定数据起始位置
if w.dataStart == 0 {
// 预留足够空间给 B+Tree 索引
// 假设索引最多占用 10% 的空间,最少 1 MB
estimatedIndexSize := int64(10 * 1024 * 1024) // 10 MB
w.dataStart = HeaderSize + estimatedIndexSize
w.dataOffset = w.dataStart
}
offset := w.dataOffset
_, err := w.file.WriteAt(compressed, offset)
if err != nil {
return err
}
// 添加到 B+Tree
err = w.builder.Add(row.Seq, offset, int32(len(compressed)))
if err != nil {
return err
}
// 更新数据偏移
w.dataOffset += int64(len(compressed))
return nil
}
// Finish 完成写入
func (w *Writer) Finish() error {
// 1. 构建 B+Tree 索引
rootOffset, err := w.builder.Build()
if err != nil {
return err
}
// 2. 计算索引大小
indexSize := w.dataStart - HeaderSize
// 3. 创建 Header
header := &Header{
Magic: MagicNumber,
Version: Version,
Compression: w.compression,
IndexOffset: HeaderSize,
IndexSize: indexSize,
RootOffset: rootOffset,
DataOffset: w.dataStart,
DataSize: w.dataOffset - w.dataStart,
RowCount: w.rowCount,
MinKey: w.minKey,
MaxKey: w.maxKey,
MinTime: w.minTime,
MaxTime: w.maxTime,
}
// 4. 写入 Header
headerData := header.Marshal()
_, err = w.file.WriteAt(headerData, 0)
if err != nil {
return err
}
// 5. Sync 到磁盘
return w.file.Sync()
}
// encodeRow 编码行数据 (使用二进制格式)
func encodeRow(row *Row) []byte {
// 使用二进制格式编码
encoded, err := encodeRowBinary(row)
if err != nil {
// 降级到 JSON (不应该发生)
data := map[string]interface{}{
"_seq": row.Seq,
"_time": row.Time,
"data": row.Data,
}
encoded, _ = json.Marshal(data)
}
return encoded
}