在 Go 生态中,Otter 和 Badger 是两个经常被同时提起的名字。它们都以高性能键值存取著称,但设计目标却截然不同:
- Otter:纯内存缓存库,追求极致的读写速度和低 GC 压力。
- Badger:持久化嵌入式键值存储,基于 LSM-Tree,兼顾写入吞吐与数据可靠性。
乍看都是 Set/Get,但背后是完全不同的工程取舍。本文从设计目标、架构、性能、适用场景四个维度进行对比,帮助你为自己的项目做出正确选择。
设计目标 —— 缓存 vs 存储
两个库最根本的区别在于对"数据生命周期"的假设:
| 维度 | Otter | Badger |
|---|---|---|
| 定位 | 进程内缓存(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)
- Window Cache:新条目先进此处,给新热点"证明自己"的机会,防止冷启动阶段频繁误逐。
- Main Cache:经过 Window 筛选后的条目,根据访问频率自适应迁移到 Probation 或 Protected 段。Protected 段的条目不会被轻易逐出。
- Count-Min Sketch + 布隆过滤器:以极小的空间代价近似记录数千万条目的访问频率,同时通过布隆过滤器过滤掉低频噪音。
关键设计亮点
- 无锁读取路径:Get 操作完全无锁,仅依赖原子操作,跨核心扩展性极佳。
- 零依赖:仅依赖 Go 标准库——没有外部依赖,构建快速可靠。
- 成本感知驱逐:支持为每个条目指定"驱逐成本",大对象优先被淘汰,避免一条大缓存冲垮整个池子。
- 逐出回调:
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 倍)。
关键设计亮点
- ACID 事务:支持 SSI(Serializable Snapshot Isolation)级别的事务,乐观并发控制。
- AES-GCM / XChaCha20-Poly1305 静态加密:数据在磁盘上透明加密,密钥支持自动轮换。
- Zstd / Snappy 压缩:SST 块级压缩,默认 Zstd。
- 流式备份与恢复:
Backup/Load支持完整和增量快照。 - 纯内存模式:
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
})
性能对比
下面从不同维度对比两者的性能表现。注意这不是一场公平的竞赛——它们优化的是不同的指标。
| 指标 | Otter | Badger |
|---|---|---|
| 读吞吐(纯内存命中) | ~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 快 10-50 倍——这在意料之中,因为 Badger 至少需要一次键查找和可能的一次磁盘 I/O。
- Badger 的写延迟相对稳定,而 Otter 在达到容量上限触发驱逐时会有微秒级的抖动。
- Badger 的读延迟方差很大——缓存命中时接近 Otter,缓存未命中时需遍历多层 LSM 树。
互补而非竞争
与其说 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,当你的场景是
- 热数据全部可以放在内存中(几十到几百万条目)
- 数据丢失可以容忍,或者可以从上游恢复
- 需要极致的读取性能和极低的 P99 延迟
- 对 GC 暂停敏感(高频交易、实时流处理)
选 Badger,当你的场景是
- 数据量超过可用内存(数十 GB 到 TB 级)
- 数据需要持久化,重启后不能丢失
- 需要事务支持和 ACID 语义
- 需要加密存储或流式备份
两者都选,当你的场景是
- 高频读取 + 大容量持久化存储
- 分层缓存架构——Otter 挡第一波流量,Badger 做后备存储
- 对延迟和容量都有苛刻要求
| 场景 | 推荐方案 |
|---|---|
| 简单的 API 响应缓存 | Otter |
| 用户会话持久化 | Badger |
| 数据库查询缓存 + 本地持久化 | Otter → Badger |
| 嵌入式时序数据存储 | Badger |
| 微服务本地缓存(可丢失) | Otter |
| 边缘设备离线存储 | Badger(纯内存模式) |
总结
Otter 和 Badger 代表了两条不同的工程路线:
- Otter 将"快"推到极致——无锁、零依赖、W-TinyLFU 驱逐策略,是 Go 内存缓存的当前最优解。
- Badger 将"稳"做到极致——LSM 树 + 值日志分离、ACID 事务、静态加密,是 Go 嵌入式持久化存储的事实标准。
理解它们的差异,不是要二选一,而是知道什么时候该用什么。许多高吞吐系统的最佳实践恰恰是让 Otter 在前面挡子弹,Badger 在后面做仓库——各司其职,珠联璧合。
参考链接: