Skip to content

Repository files navigation

MiniKV

MiniKV 是一个使用 Go 实现的轻量级嵌入式 KV 存储组件,适合保存任务状态、checkpoint 和临时运行数据。它以 map[string]string 作为内存索引,通过 WAL 提供重启恢复能力,并实现了 TTL、Batch、Snapshot 和 Compaction 等完整的基础存储流程。

这个项目的目标是用较小的代码规模把存储组件的关键问题做清楚。它不是 Redis 的替代品,也不是分布式数据库。

功能特性

  • SetGetDeleteSetWithTTL 基础操作。
  • Batch 批量写入,整批校验后以单条 WAL 记录提交。
  • 追加式 JSON Lines WAL,每条新记录带 CRC32 checksum。
  • 启动时 replay WAL,恢复最终内存状态。
  • 识别并截断尾部半写记录;拒绝中间损坏和 checksum 不一致的记录。
  • TTL 使用绝对过期时间,结合惰性删除和后台定期清理。
  • Snapshot + 增量 WAL 恢复,减少长 WAL 的启动回放成本。
  • 手动 Compaction 和按 WAL 大小触发的自动 Compaction。
  • 数据目录独占,避免两个 Store 同时写入同一份 WAL。
  • 单元测试、race 检测和 benchmark。

快速开始

要求 Go 1.22 或更高版本。

go get github.com/Yangsss13/miniKV
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/Yangsss13/miniKV"
)

func main() {
    store, err := minikv.OpenWithOptions("./data", minikv.Options{
        CleanupInterval:     time.Minute,
        AutoCompactMinBytes: 64 * 1024 * 1024,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer store.Close()

    if err := store.Set("task:1", "running"); err != nil {
        log.Fatal(err)
    }

    value, ok := store.Get("task:1")
    if ok {
        fmt.Println(value)
    }

    if err := store.SetWithTTL("task:2", "pending", 10*time.Second); err != nil {
        log.Fatal(err)
    }
}

批量写入:

err := store.Batch([]minikv.BatchOp{
    {Op: minikv.BatchSet, Key: "task:1", Value: "running"},
    {Op: minikv.BatchSetTTL, Key: "task:2", Value: "temporary", TTL: time.Minute},
    {Op: minikv.BatchDelete, Key: "task:old"},
})

手动生成 Snapshot 或压缩 WAL:

if err := store.Snapshot(); err != nil {
    log.Fatal(err)
}

if err := store.Compact(); err != nil {
    log.Fatal(err)
}

核心设计

写入流程

SetDeleteSetWithTTLBatch 遵循同一条写入规则:

校验输入
  -> 获取写锁
  -> 追加 WAL 记录
  -> Sync 刷盘
  -> 更新 data/expires
  -> 释放写锁
  -> 按配置检查是否需要自动 Compaction

只有 WAL 写入和 Sync 成功后,MiniKV 才更新内存,避免当前进程看到无法在重启后恢复的新状态。每次写入都调用 Sync,提高了恢复可靠性,也直接限制了写吞吐量。

WAL 与恢复

WAL 使用一行一条 JSON 记录的格式。新记录将操作和 CRC32 checksum 包装在一起:

{"record":{"op":"set","key":"task:1","value":"running"},"checksum":123456789}

MiniKV 仍可读取旧版无 checksum 的 WAL 记录。恢复时必须按写入顺序 replay,因为同一个 key 的最终状态取决于最后一次有效操作。

Open(dir) 的恢复流程为:

创建空的 data/expires
  -> 打开 WAL
  -> 存在 Snapshot 时先加载 Snapshot
  -> 从 Snapshot 记录的 wal_offset 继续 replay WAL
  -> 没有 Snapshot 时从 WAL 开头 replay
  -> 将文件指针移到末尾
  -> 启动 TTL cleaner

如果最后一条 WAL 因半写而不完整,恢复会将其截断,视为该操作没有成功提交;如果损坏发生在中间,或完整记录的 checksum 不一致,恢复会直接报错,避免返回不可信状态。

TTL

MiniKV 使用两张 map 保存当前状态:

data    map[string]string
expires map[string]time.Time

永久 key 只存在于 data,TTL key 还会在 expires 中保存绝对过期时间 expire_at。记录绝对时间可以避免程序重启后重新计算完整 TTL,错误延长 key 的生命周期。

过期数据通过两种方式清理:

  • 惰性删除:Get 发现 key 已过期时立即删除并返回不存在。
  • 定期删除:后台 cleaner 定期扫描 expires,删除无人访问的过期 key。

Snapshot 与 Compaction

两者解决的问题不同:

机制 解决的问题 实现方式
Snapshot 减少启动恢复时需要 replay 的记录 保存当前 data/expires 和对应的 wal_offset
Compaction 控制 WAL 文件持续增长 根据当前有效状态重写一份精简 WAL

Snapshot 之后的写入仍然追加到 WAL。恢复时先加载 Snapshot,再 replay wal_offset 之后的增量记录。Compaction 会改变整个 WAL 的位置,因此完成后会移除 offset 已失效的旧 Snapshot。

Snapshot 和 Compaction 都持有写锁,以保证生成的磁盘状态与同一时刻的内存状态一致。

Batch

Batch 会先校验整批操作,再把整批内容编码成一条 WAL 记录,成功刷盘后统一更新内存。这样可以避免非法操作导致内存只应用半批,也避免恢复时只 replay 到批次的一部分。

它不是带回滚和隔离级别的数据库事务;如果底层 Sync 返回错误,调用方应把本次提交结果视为不确定,并通过重试或重新打开 Store 确认状态。

Checkpoint 场景

上层任务系统可以把少量运行时恢复状态编码为 JSON 后写入 MiniKV:

checkpoint:task:42 -> {"task_id":"task:42","current_step":2,"status":"running"}

服务重启后重新打开同一数据目录即可读取 checkpoint。任务定义、执行日志等业务事实仍应由上层系统保存;MiniKV 只负责 checkpoint 的持久化和恢复,不决定业务从哪一步继续执行。

测试

go test ./...
go test -race ./...
go test -bench BenchmarkStore -benchmem -run none -count=1 .

测试覆盖基础读写、参数校验、关闭状态、WAL 恢复、TTL、Batch、Snapshot、Compaction、目录独占、checkpoint、并发访问,以及尾部半写和中间损坏等失败场景。

2026-07-15 本地 benchmark(Windows/amd64,13th Gen Intel Core i9-13980HX):

BenchmarkStoreSet-32                     306115 ns/op      714 B/op      8 allocs/op
BenchmarkStoreGet-32                         70.79 ns/op    13 B/op      1 allocs/op
BenchmarkStoreSetWithTTL-32              373960 ns/op      919 B/op      7 allocs/op
BenchmarkStoreBatch-32                   302345 ns/op     1947 B/op     18 allocs/op
BenchmarkStoreCompact-32               14085819 ns/op     8802 B/op     90 allocs/op
BenchmarkStoreRecoverFromFullWAL-32     3122417 ns/op   921116 B/op  14057 allocs/op
BenchmarkStoreRecoverFromSnapshot-32    1447365 ns/op   388946 B/op   3224 allocs/op

结果只反映该机器和当前测试数据。写操作包含 WAL Sync,因此明显慢于内存读取;在这组 1000 条基础记录和 10 条增量记录的样本中,Snapshot 恢复比全量 WAL replay 耗时更低、内存分配更少。

项目边界

MiniKV 有意保持较小范围,目前不实现 LSM Tree、MVCC、SQL 事务、Raft、复制和分布式存储。

当前实现仍有以下限制:

  • JSON Lines WAL 易于阅读和调试,但空间利用率和编解码效率不如二进制格式。
  • 每次写入都调用 Sync,可靠性优先,但不适合高吞吐写入场景。
  • Get 可能触发惰性删除,因此当前使用写锁,读并发仍有优化空间。
  • 定期清理会扫描全部 TTL key;大规模 TTL 场景可进一步使用分批扫描、最小堆或时间轮。
  • Snapshot 需要手动触发,Compaction 期间也会阻塞其他读写。
  • 目录锁使用操作系统级排他锁;标记文件可以保留,但进程退出后锁会由系统释放,重启不需要人工删除锁文件。
  • 临时文件替换流程适合当前学习项目,生产级存储还需要更严格的目录同步、故障注入和跨平台原子替换验证。

About

MiniKV 是一个基于 Go 的轻量级嵌入式 KV 存储组件,定位是 MiniKVX-Agent 的底层运行状态存储,同时也作为独立技术项目展示

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages