leveldb 不能直接用作热点缓存,因其缺乏 ttl、lru 驱逐和并发写保护;需在 echo 中间件中自行实现过期校验、串行写入和统一缓存逻辑。

直接用 Echo + LevelDB 做“热点缓存”容易踩坑——LevelDB 本身不是缓存,而是持久化引擎;它没有 TTL、LRU 驱逐、并发读写保护等缓存必需能力。真要落地,得自己补足这些能力,否则上线后会遇到数据陈旧、goroutine panic、磁盘打满等问题。
LevelDB 在 Go 中不能直接当缓存用
Go 生态里没有官方 LevelDB 绑定,主流是 syndtr/goleveldb(纯 Go 实现)或 jenkins-zh/levigo(Cgo 封装)。但无论哪种:
-
goleveldb不支持 concurrent write —— 多个 goroutine 同时Put会 panic,必须外层加sync.RWMutex或用 channel 串行化 - LevelDB 没有内置过期机制:
Get永远返回最新写入值,哪怕这值是 3 天前存的 - 它不区分“热”和“冷”:所有 key 一视同仁刷盘,高频更新的 key 会引发大量 minor compaction,拖慢整体吞吐
- 默认
block_cache是 C++ 层的 LRU,Go 绑定通常不暴露该配置项,你无法控制内存占用上限
用 Echo 中间件封装 LevelDB 缓存逻辑
想让 LevelDB “像缓存一样用”,必须在 Echo 的 echo.MiddlewareFunc 里做三件事:检查请求是否命中、读取并校验时效、未命中则回源并写入。关键点:
- 时效判断不能靠 LevelDB 自身:需把过期时间(如
int64时间戳)和 value 一起序列化存入,例如用gob编码struct{Val interface{}; ExpireAt int64} - 写入前必须加锁:建议用
sync.Map管理正在写入的 key,避免重复 compaction;或者用单 goroutine 处理写入队列(chan [2]string) - 不要在
GET /api/user/:id这类路由里直接db.Get:应统一走中间件,否则缓存逻辑散落各处,后续无法统一加监控或降级 - 示例片段(简化):
func LevelDBCache(db *leveldb.DB, ttl time.Duration) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { key := c.Request().URL.Path data, err := db.Get([]byte(key), nil) if err == nil { var item cacheItem if err = gob.DecodeBytes(data, &item); err == nil && time.Now().Unix()
为什么不该用 LevelDB 替代 BigCache 或 Freecache
如果你的场景是“千 QPS 以上、百 GB 热点数据、毫秒级响应”,LevelDB 反而是瓶颈:
- 每次
Get都触发至少一次磁盘pread(即使有 page cache),而BigCache是纯内存 hash table,延迟稳定在 100ns 级别 - LevelDB 的 WAL 日志 + 多层 SSTable 导致写放大明显;高并发写入时 CPU 和 I/O 负载陡增,
freecache内存池复用可压到 GC 压力极低 - 故障恢复成本高:LevelDB 崩溃后需 replay log,而内存缓存挂了直接重建,无状态
- 真正需要 LevelDB 的地方是“缓存穿透兜底”——比如用它存 fallback 数据,配合
cache2go做主缓存,而不是直接扛流量
LevelDB 的定位始终是嵌入式持久化引擎,不是缓存。把它塞进 Echo 的中间件链,等于把硬盘当内存使——能跑通,但稍有规模就会暴露设计错位。真正省事又可靠的做法:主缓存用 BigCache,穿透兜底用 goleveldb,两者之间用简单接口隔离,别试图让 LevelDB 假扮缓存。











