主要改动: - 重构目录结构:合并子目录到根目录,简化项目结构 - 添加完整的查询 API:支持复杂条件查询、字段选择、游标模式 - 实现 LSM-Tree Compaction:7层结构、Score-based策略、后台异步合并 - 添加 Web UI:基于 Lit 的现代化管理界面,支持数据浏览和 Manifest 查看 - 完善文档:添加 README.md 和 examples/webui/README.md 新增功能: - Query Builder:链式查询 API,支持 Eq/Lt/Gt/In/Between/Contains 等操作符 - Web UI 组件:srdb-app、srdb-table-list、srdb-data-view、srdb-manifest-view 等 - 列选择持久化:自动保存到 localStorage - 刷新按钮:一键刷新当前视图 - 主题切换:深色/浅色主题支持 代码优化: - 使用 Go 1.24 新特性:range 7、min()、maps.Copy()、slices.Sort() - 统一组件命名:所有 Web Components 使用 srdb-* 前缀 - CSS 优化:提取共享样式,减少重复代码 - 清理遗留代码:删除未使用的方法和样式
409 lines
7.5 KiB
Go
409 lines
7.5 KiB
Go
package srdb
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
// Entry 类型
|
|
WALEntryTypePut = 1
|
|
WALEntryTypeDelete = 2 // 预留,暂不支持
|
|
|
|
// Entry Header 大小
|
|
WALEntryHeaderSize = 17 // CRC32(4) + Length(4) + Type(1) + Seq(8)
|
|
)
|
|
|
|
// WALEntry WAL 条目
|
|
type WALEntry struct {
|
|
Type byte // 操作类型
|
|
Seq int64 // _seq
|
|
Data []byte // 数据
|
|
CRC32 uint32 // 校验和
|
|
}
|
|
|
|
// WAL Write-Ahead Log
|
|
type WAL struct {
|
|
file *os.File
|
|
offset int64
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// OpenWAL 打开 WAL 文件
|
|
func OpenWAL(path string) (*WAL, error) {
|
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 获取当前文件大小
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
file.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return &WAL{
|
|
file: file,
|
|
offset: stat.Size(),
|
|
}, nil
|
|
}
|
|
|
|
// Append 追加一条记录
|
|
func (w *WAL) Append(entry *WALEntry) error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
// 序列化 Entry
|
|
data := w.marshalEntry(entry)
|
|
|
|
// 写入文件
|
|
_, err := w.file.Write(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.offset += int64(len(data))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sync 同步到磁盘
|
|
func (w *WAL) Sync() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
return w.file.Sync()
|
|
}
|
|
|
|
// Close 关闭 WAL
|
|
func (w *WAL) Close() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
return w.file.Close()
|
|
}
|
|
|
|
// Truncate 清空 WAL
|
|
func (w *WAL) Truncate() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
err := w.file.Truncate(0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = w.file.Seek(0, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.offset = 0
|
|
return nil
|
|
}
|
|
|
|
// marshalEntry 序列化 Entry
|
|
func (w *WAL) marshalEntry(entry *WALEntry) []byte {
|
|
dataLen := len(entry.Data)
|
|
totalLen := WALEntryHeaderSize + dataLen
|
|
|
|
buf := make([]byte, totalLen)
|
|
|
|
// 计算 CRC32 (不包括 CRC32 字段本身)
|
|
crcData := buf[4:totalLen]
|
|
binary.LittleEndian.PutUint32(crcData[0:4], uint32(dataLen))
|
|
crcData[4] = entry.Type
|
|
binary.LittleEndian.PutUint64(crcData[5:13], uint64(entry.Seq))
|
|
copy(crcData[13:], entry.Data)
|
|
|
|
crc := crc32.ChecksumIEEE(crcData)
|
|
|
|
// 写入 CRC32
|
|
binary.LittleEndian.PutUint32(buf[0:4], crc)
|
|
|
|
return buf
|
|
}
|
|
|
|
// WALReader WAL 读取器
|
|
type WALReader struct {
|
|
file *os.File
|
|
}
|
|
|
|
// NewWALReader 创建 WAL 读取器
|
|
func NewWALReader(path string) (*WALReader, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &WALReader{
|
|
file: file,
|
|
}, nil
|
|
}
|
|
|
|
// Read 读取所有 Entry
|
|
func (r *WALReader) Read() ([]*WALEntry, error) {
|
|
var entries []*WALEntry
|
|
|
|
for {
|
|
entry, err := r.readEntry()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// Close 关闭读取器
|
|
func (r *WALReader) Close() error {
|
|
return r.file.Close()
|
|
}
|
|
|
|
// readEntry 读取一条 Entry
|
|
func (r *WALReader) readEntry() (*WALEntry, error) {
|
|
// 读取 Header
|
|
header := make([]byte, WALEntryHeaderSize)
|
|
_, err := io.ReadFull(r.file, header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 解析 Header
|
|
crc := binary.LittleEndian.Uint32(header[0:4])
|
|
dataLen := binary.LittleEndian.Uint32(header[4:8])
|
|
entryType := header[8]
|
|
seq := int64(binary.LittleEndian.Uint64(header[9:17]))
|
|
|
|
// 读取 Data
|
|
data := make([]byte, dataLen)
|
|
_, err = io.ReadFull(r.file, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 验证 CRC32
|
|
crcData := make([]byte, WALEntryHeaderSize-4+int(dataLen))
|
|
copy(crcData[0:WALEntryHeaderSize-4], header[4:])
|
|
copy(crcData[WALEntryHeaderSize-4:], data)
|
|
|
|
if crc32.ChecksumIEEE(crcData) != crc {
|
|
return nil, io.ErrUnexpectedEOF // CRC 校验失败
|
|
}
|
|
|
|
return &WALEntry{
|
|
Type: entryType,
|
|
Seq: seq,
|
|
Data: data,
|
|
CRC32: crc,
|
|
}, nil
|
|
}
|
|
|
|
// WALManager WAL 管理器,管理多个 WAL 文件
|
|
type WALManager struct {
|
|
dir string
|
|
currentWAL *WAL
|
|
currentNumber int64
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewWALManager 创建 WAL 管理器
|
|
func NewWALManager(dir string) (*WALManager, error) {
|
|
// 确保目录存在
|
|
err := os.MkdirAll(dir, 0755)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 读取当前 WAL 编号
|
|
number, err := readWALCurrentNumber(dir)
|
|
if err != nil {
|
|
// 如果读取失败,从 1 开始
|
|
number = 1
|
|
}
|
|
|
|
// 打开当前 WAL
|
|
walPath := filepath.Join(dir, fmt.Sprintf("%06d.wal", number))
|
|
wal, err := OpenWAL(walPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 保存当前编号
|
|
err = saveWALCurrentNumber(dir, number)
|
|
if err != nil {
|
|
wal.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return &WALManager{
|
|
dir: dir,
|
|
currentWAL: wal,
|
|
currentNumber: number,
|
|
}, nil
|
|
}
|
|
|
|
// Append 追加记录到当前 WAL
|
|
func (m *WALManager) Append(entry *WALEntry) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return m.currentWAL.Append(entry)
|
|
}
|
|
|
|
// Sync 同步当前 WAL 到磁盘
|
|
func (m *WALManager) Sync() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return m.currentWAL.Sync()
|
|
}
|
|
|
|
// Rotate 切换到新的 WAL 文件
|
|
func (m *WALManager) Rotate() (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// 记录旧的 WAL 编号
|
|
oldNumber := m.currentNumber
|
|
|
|
// 关闭当前 WAL
|
|
err := m.currentWAL.Close()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// 创建新 WAL
|
|
m.currentNumber++
|
|
walPath := filepath.Join(m.dir, fmt.Sprintf("%06d.wal", m.currentNumber))
|
|
wal, err := OpenWAL(walPath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
m.currentWAL = wal
|
|
|
|
// 更新 CURRENT 文件
|
|
err = saveWALCurrentNumber(m.dir, m.currentNumber)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return oldNumber, nil
|
|
}
|
|
|
|
// Delete 删除指定的 WAL 文件
|
|
func (m *WALManager) Delete(number int64) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
walPath := filepath.Join(m.dir, fmt.Sprintf("%06d.wal", number))
|
|
return os.Remove(walPath)
|
|
}
|
|
|
|
// GetCurrentNumber 获取当前 WAL 编号
|
|
func (m *WALManager) GetCurrentNumber() int64 {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
return m.currentNumber
|
|
}
|
|
|
|
// RecoverAll 恢复所有 WAL 文件
|
|
func (m *WALManager) RecoverAll() ([]*WALEntry, error) {
|
|
// 查找所有 WAL 文件
|
|
pattern := filepath.Join(m.dir, "*.wal")
|
|
files, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// 按文件名排序(确保按时间顺序)
|
|
sort.Strings(files)
|
|
|
|
var allEntries []*WALEntry
|
|
|
|
// 依次读取每个 WAL
|
|
for _, file := range files {
|
|
reader, err := NewWALReader(file)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
entries, err := reader.Read()
|
|
reader.Close()
|
|
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
allEntries = append(allEntries, entries...)
|
|
}
|
|
|
|
return allEntries, nil
|
|
}
|
|
|
|
// ListWALFiles 列出所有 WAL 文件
|
|
func (m *WALManager) ListWALFiles() ([]string, error) {
|
|
pattern := filepath.Join(m.dir, "*.wal")
|
|
files, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sort.Strings(files)
|
|
return files, nil
|
|
}
|
|
|
|
// Close 关闭 WAL 管理器
|
|
func (m *WALManager) Close() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.currentWAL != nil {
|
|
return m.currentWAL.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readWALCurrentNumber 读取当前 WAL 编号
|
|
func readWALCurrentNumber(dir string) (int64, error) {
|
|
currentPath := filepath.Join(dir, "CURRENT")
|
|
data, err := os.ReadFile(currentPath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
number, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return number, nil
|
|
}
|
|
|
|
// saveWALCurrentNumber 保存当前 WAL 编号
|
|
func saveWALCurrentNumber(dir string, number int64) error {
|
|
currentPath := filepath.Join(dir, "CURRENT")
|
|
data := fmt.Appendf(nil, "%d\n", number)
|
|
return os.WriteFile(currentPath, data, 0644)
|
|
}
|