在 Go 生态中,OtterBadger 是两个经常被同时提起的名字。它们都以高性能键值存取著称,但设计目标却截然不同:

乍看都是 Set/Get,但背后是完全不同的工程取舍。本文从设计目标、架构、性能、适用场景四个维度进行对比,帮助你为自己的项目做出正确选择。


设计目标 —— 缓存 vs 存储

两个库最根本的区别在于对"数据生命周期"的假设:

维度OtterBadger
定位进程内缓存(Cache)嵌入式持久化 KV 存储(Database)
数据持久性不保证——进程退出即丢失默认持久化到磁盘,可选纯内存模式
驱逐策略W-TinyLFU 自适应驱逐无自动驱逐(除非设置 TTL)
容量上限固定最大条目数(驱逐触发边界)磁盘容量
核心场景加速热点数据访问,降低外部延迟替代传统数据库进行本地持久化存储

Otter 的答案是:“我很快,但我记不住东西。”

Badger 的答案是:“我够快,而且我绝不会忘记。”


Otter:无锁 W-TinyLFU 缓存

Otter 是 Go 生态中第一个完整实现 无锁 W-TinyLFU 驱逐策略的缓存库。它的设计哲学非常纯粹——用极致的工程优化做一件事:在给定内存预算下最大化缓存命中率

核心架构

Shard 0 (无锁)
├── Window Cache (LRU, 1% 空间)
├── Main Cache (SLRU, 99% 空间)
│   ├── Probation 段
│   └── Protected 段
└── Count-Min Sketch (频率统计)

Shard 1 ... Shard 15 (同上,共 16 个 Shard)

关键设计亮点

  1. 无锁读取路径:Get 操作完全无锁,仅依赖原子操作,跨核心扩展性极佳。
  2. 零依赖:仅依赖 Go 标准库——没有外部依赖,构建快速可靠。
  3. 成本感知驱逐:支持为每个条目指定"驱逐成本",大对象优先被淘汰,避免一条大缓存冲垮整个池子。
  4. 逐出回调DeletionListener 在条目被逐出时通知,适合做资源清理或回写。
cache, err := otter.MustBuilder[string, Response](100_000).
    CollectStats().
    WithTTL(5 * time.Minute).
    WithCost(func(key string, value Response) uint32 {
        return uint32(len(value.Body)) // 按大小计算成本
    }).
    DeletionListener(func(key string, value Response, cause otter.DeletionCause) {
        log.Printf("evicted: %s, cause: %v", key, cause)
    }).
    Build()

Badger:LSM-Tree 持久化 KV 存储

Badger 是 Dgraph 团队打造的嵌入式持久化 KV 存储。它受 RocksDB/LevelDB 启发,但用纯 Go 重写,并针对 SSD 和 Go 运行时做了深度优化。

核心架构

WAL (Write-Ahead Log)
MemTable (内存中排序的跳表)
    │  (满后刷盘)
SST Files (层级 0 → 层级 6)
Value Log (vlog) ← 大值单独存储,减少写放大

Badger 最独特的设计是 键值分离(Value Log)

LSM 树 (SST)        值日志 (vlog, 仅追加)
┌──────────────┐    ┌──────────────────┐
│ key → vlog_off│    │ value1           │
│ key → vlog_off│    │ value2           │
│ key → vlog_off│    │ value3           │
└──────────────┘    └──────────────────┘

小(键 + 指针)存 LSM 树,大值存仅追加的 vlog。这极大降低了 LSM 写放大(Badger 约 3-5 倍,RocksDB 约 10-50 倍)。

关键设计亮点

  1. ACID 事务:支持 SSI(Serializable Snapshot Isolation)级别的事务,乐观并发控制。
  2. AES-GCM / XChaCha20-Poly1305 静态加密:数据在磁盘上透明加密,密钥支持自动轮换。
  3. Zstd / Snappy 压缩:SST 块级压缩,默认 Zstd。
  4. 流式备份与恢复Backup / Load 支持完整和增量快照。
  5. 纯内存模式WithInMemory(true) 可将其作为高性能内存数据库使用。
db, _ := badger.Open(badger.DefaultOptions("/data/badger").
    WithEncryptionKey(myKey).
    WithCompression(badger.Zstd))

// 读写事务
db.Update(func(txn *badger.Txn) error {
    e := badger.NewEntry([]byte("user:42"), userJSON).
        WithTTL(24 * time.Hour)
    return txn.SetEntry(e)
})

// 只读事务 + 迭代器
db.View(func(txn *badger.Txn) error {
    item, _ := txn.Get([]byte("user:42"))
    val, _ := item.ValueCopy(nil)
    return nil
})

性能对比

下面从不同维度对比两者的性能表现。注意这不是一场公平的竞赛——它们优化的是不同的指标。

指标OtterBadger
读吞吐(纯内存命中)~5000万 ops/s~200万 ops/s(缓存命中时)
写吞吐~3000万 ops/s~50万 ops/s(顺序写入)
P99 延迟(读)< 1µs~5-50µs(取决于是否需读盘)
内存开销极低(无 GC 扫描根)中等(MemTable + 缓存)
写放大无(纯内存操作)~3-5 倍(vlog 分离后)
数据安全进程退出丢失持久化到磁盘,WAL 保护

数据来源:Otter 官方基准测试(对比 Ristretto / FreeCache / BigCache)及 Badger 官方 benchmark。

关键观察


互补而非竞争

与其说 Otter 和 Badger 是竞争对手,不如说它们 可以完美互补。一个经典的架构模式是:

应用层
  ├── Otter (L1 内存缓存 — 热数据,纳秒级访问)
  ├── Badger (L2 磁盘存储 — 温数据,毫秒级持久化)
  └── 外部数据库 (L3 持久化存储 — 冷数据,远程访问)

Otter 作为 Badger 的前置缓存:利用 Otter 的 W-TinyLFU 策略过滤出最热的数据条目,只有缓存未命中时才回源到 Badger。这既能获得 Otter 的极速读取,又能享受 Badger 的数据持久性。

type CacheWithFallback struct {
    hot  *otter.Cache[string, []byte]   // L1: 内存
    warm *badger.DB                       // L2: 磁盘
}

func (c *CacheWithFallback) Get(key string) ([]byte, error) {
    if val, ok := c.hot.Get(key); ok {
        return val, nil   // L1 命中,亚微秒返回
    }
    // L1 未命中,查 Badger
    var val []byte
    err := c.warm.View(func(txn *badger.Txn) error {
        item, err := txn.Get([]byte(key))
        if err != nil { return err }
        val, err = item.ValueCopy(nil)
        return err
    })
    if err != nil { return nil, err }
    // 回填 L1 缓存
    c.hot.Set(key, val)
    return val, nil
}

这种 分层缓存架构 在 Go 生态中非常实用——用 Otter 扛高频流量,用 Badger 做近线存储扩展,兼顾速度与容量。


如何选择

选 Otter,当你的场景是

选 Badger,当你的场景是

两者都选,当你的场景是

场景推荐方案
简单的 API 响应缓存Otter
用户会话持久化Badger
数据库查询缓存 + 本地持久化Otter → Badger
嵌入式时序数据存储Badger
微服务本地缓存(可丢失)Otter
边缘设备离线存储Badger(纯内存模式)

总结

Otter 和 Badger 代表了两条不同的工程路线:

理解它们的差异,不是要二选一,而是知道什么时候该用什么。许多高吞吐系统的最佳实践恰恰是让 Otter 在前面挡子弹,Badger 在后面做仓库——各司其职,珠联璧合。


参考链接:

  1. maypok86/otter — GitHub
  2. dgraph-io/badger — GitHub
  3. Otter 基准测试
  4. Badger 文档