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:
98
sst/encoding.go
Normal file
98
sst/encoding.go
Normal 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
117
sst/encoding_test.go
Normal 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
142
sst/format.go
Normal 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
284
sst/manager.go
Normal 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
152
sst/reader.go
Normal 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
183
sst/sst_test.go
Normal 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
155
sst/writer.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user