跳到主要内容

引言

在 Go 中我们可以直接使用 map 来做本地缓存,但是相对于专门的本地缓存组件来说 map 的性能较低、不是并发安全、没有内存淘汰策略。 所以,使用一些专门的本地缓存组件是一个更佳的选择。

技术选型

选型时我们需要做充分的调研分析,Go 的开源本地缓存组件不少,经过调研,这里挑选了三款 Start 数较高的来做个简单介绍,分别是 FreeCacheBigCacheFastCache。 他们都是零 GC 开销,并发安全,适用于高并发读多写少的场景。具体可以去其 github 主页查看更多介绍。这里我们来列个表格做对比:

FreeCacheBigCacheFastCache
存储容量限制需要预分配容量且不能扩容分片可以自动扩容预分配每个 Key 为 64K
过期策略可以对每个 Key 设置过期时间只能对桶进行过期时间设置,无法对单个 Key 设置不支持过期时间设置
容量淘汰机制LRU 结合 Set 时触发FIFO(Set 时触发 + 定期删除)FIFO(Set 时触发 + 定时删除)
锁机制分片 + 互斥锁分片 + 读写锁分片 + 读写锁
磁盘备份支持官网说 TODO不支持支持
  • FreeCahce 的优势是可以对每个 Key 单独设置过期时间,但大量带有过期时间的 Key 并不是免费的,是有成本开销的, 因为过期策略的内部维护了定时器,定时器轮询会占用 CPU,当定时器多了之后资源就浪费在定时器上面了。 FreeCache 是三者中基准性能最差的,适用于一些短的过期时间的缓存场景,段时间内用完即销毁。

  • BigCache 以桶为单位,一个桶里面存放多个 Key,可以为一个桶设置过期时间但不能为单个 Key 设置过期时间,支持容量自动扩容,不过扩容操作也不是免费的,会有额外开销。 相比 FreeCache 而言 BigCache 的过期粒度更粗,如果使用姿势不对,性能就会大打折扣。 比如每个桶我就存一个 Key,有多少个 Key 就开多少个桶,变相实现 Key 级别的过期策略,是铁都给它撸出血。 BigCache 适用于分组进行缓存,例如按用户进行缓存,当一个用户活跃时为他开一个桶,不活跃了则整个桶过期。

  • FastCache 的设计参考了 BigCache,64K 的条目限制减少了内存碎片的产生,虽然看起来功能最简陋,但是性能最好, 跑得快的往往是裸奔的,因为没有裤子卡裆。FastCache 适用于那些需要常驻内存的场景,例如展示最新 100 条数据。

综上所述,每个缓存组件都有自己的侧重点,不同的缓存组件适用于不同的业务场景。难道就没有一款万能的缓存组件,把三种缓存的特性都实现的吗? 如果你有这种想法那太好了,因为 Goodle 把三者整合到一起了,做了一个上层包装,提供统一调用方式,这不就万能了。

使用示例

main.go
import (
"github.com/text3cn/goodle/goodle"
"github.com/text3cn/goodle/providers/cache"
"github.com/text3cn/goodle/providers/httpserver"
)

func main() {
// 存储桶容量,字节为单位:100M
bucketSize := 100 * 1024 * 1024
// 创建一个桶
cache.NewFreeCache("bucket1", bucketSize)

// 启动 http 服务
goodle.Init().RunHttp(func(engine *httpserver.Engine) {
engine.Get("/", Index)
}, ":3333")
}

func Index(ctx *httpserver.Context) {
// 获取桶
bucket := ctx.Cache.FreeCache("bucket1")

// 设置值,过期时间:60 秒
bucket.Set("key1", "value1", 60)

// 获取值
rs := bucket.Get("key1")
println(rs)

// 删除 key
bucket.Delete("key1")

// 清空桶
bucket.Clear()
}

作者主页提示如果你分配了大量内存,可能需要调用 debug.SetGCPercent() 来获得一个正常的 GC 频率。 这是 Go runtime 包的一个函数,用于设置缓存的垃圾回收(GC)百分比阈值。垃圾回收是Go语言的内存管理机制之一,用于释放不再使用的内存以供后续使用。 较低的百分比值可能导致更频繁的垃圾回收,会降低性能。较高的百分比值可能会减少垃圾回收的频率,但会导致更多的内存被占用。

虽然通过设置 GC 百分比阈值可以影响 FreeCache 的垃圾回收行为,但还影响程序本身的 GC,它不应该在生产环境中频繁更改,设置不当反而适得其反。 在大多数情况下 FreeCache 的默认垃圾回收设置足够满足需求,如果有大内存缓存使用需求,我们可以选择换用 BigCache。

经验

  1. 对于每秒上万并发的极热数据,如果直接跨机器调 Redis 会产生夸机器的 IO,对 Redis 产生压力,通过本地高速缓存可以有效降低 Redis 压力增加响应速度。

  2. 本地缓存不能滥用,如果业务服务器机器内存比较少,业务逻辑本身比较吃内存,特别是分布式环境下,多个实例大量使用本地缓存会冗余地浪费内存。

  3. 缓存一般都是读多写少,如果你是写多读少的场景,应该考虑使用消息队列。

总结

这些本地缓存实现高性能的关键是对数据进行分片以降低锁的粒度以及避免 GC,比直接用 map 做缓存好得多。 我们应该结合实际场景,灵活选用不同类型的缓存,Goodle 已经把他们集成到了框架里面。