Go 中使用 singleflight 防止缓存击穿
一个典型的使用缓存的案例:
// GetConfig 获取 key 指定的配置项
func GetConfig(key string) (Config, bool) {
cacheKey := "cfg:" + key
// 如果缓存中查到则直接返回缓存数据
if v, has := Cache().Get(cacheKey); has {
if cfg, ok := v.(Config); ok {
return cfg, true
}
}
// 不在缓存中则从数据库中查找
cfg := Config{}
if err := DB().Where("`key`=?", key).First(&cfg).Error; err != nil {
return cfg, false
}
// 将数据库中查到的值写入缓存, 300 秒过期
Cache().Store(cacheKey, cfg, 300)
return cfg, true
}
但这样存在一个问题: 如果 300 秒缓存过期时,有大量并发产生,将会导致这些并发在缓存中都找不到数据去查数据库的情况,从而发生所谓的缓存击穿。
我们可以使用 golang.org/x/sync/singleflight
来解决这个问题, singleflight
中主要是一个 Group
结构体,它包含以下三个函数:
// Do 执行并返回函数 fn 的结果,并确保在相同 key 在并发调用执行时,只会有一个调用被真正执行,
// 其它重复调用将会直接使用第一个执行的结果返回
// 返回的第 3 个参数 shared 表示返回的结果是直接执行得到的还是其它并发调用共享的。
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)
// DoChan Do 的 channel 返回值形式(异步)
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
// Forget 用于删除指定 key,删除后其它并发调用者将会被真正执行
func (g *Group) Forget(key string)
将 GetConfig()
重命名为 doGetConfig()
,然后重新写一个 GetConfig()
函数:
var sg *singleflight.Group
func init() {
sg = new(singleflight.Group)
}
// GetConfig 获取 key 指定的配置项
func GetConfig(key string) (Config, bool) {
val, err, _ := sg.Do("getCfg:"+key, func() (interface{}, error) {
cfg, _ := doGetConfig(key)
return cfg, nil
})
if err != nil {
// ...
}
cfg, ok := val.(Config)
if !ok {
// ...
}
return cfg, true
}