缓存更新问题
缓存是高并发业务的基石,当访问量突然上升的时候,缓存失效回源时会将请求打到后台数据库,导致服务器响应延迟或者宕机的情况。
通常缓存更新方案:
- 1.业务代码中,根据key从缓存拿不到数据,访问存储层获取数据后更新缓存
- 2.由专门的定时脚本在缓存失效前对其进行更新
- 3.通过分布式锁,实现只有一个请求负责缓存更新,其他请求等待:一种基于哨兵的缓存访问策略
通常获取缓存这样写:
1 2 3 4
| data = getCache(key) if !data { data = selectDB(key) }
|
并发获取
当缓存失效,集中查询DB时,此时需要考虑加锁。
加锁
1 2 3 4 5 6 7 8 9 10 11 12 13
| func checkAccess(key) bool { startTime = time.now() for true { if isLock(key) == false { setLock(key) return true } if (time.now() - startTime) > _maxLockTime { return false } time.sleep(20ms) } }
|
在读取缓存失败,查询 DB 之前,先来一个锁判断,如果锁不存在,那么就加把锁,再去查 DB。如果锁存在,那么就等待,然后回头再去读 redis 或者进入 DB 查询。
singleflight
上面的代码使用起来没什么问题,但是依靠无限循环 + sleep 实现的方法比较低效。而在 go 语言中,借助非常轻量和高效的协程,可以很优雅的实现这种功能,这就是 singleflight。
使用方法
使用方法很简单,可以参考其test :
1 2 3 4 5 6 7 8 9 10 11 12
| func TestDo(t *testing.T) { var g Group v, err := g.Do("key", func() (interface{}, error) { return "bar", nil }) if got, want := fmt.Sprintf("%v (%T)", v, v), "bar (string)"; got != want { t.Errorf("Do = %v; want %v", got, want) } if err != nil { t.Errorf("Do error = %v", err) } }
|
可见使用起来非常简单,对外只需要这个Do 函数,传入 Key 和获取缓存的回调函数,如此 singleflight 就能自动帮我们处理同时请求下游服务的问题了。
那么这个Do函数到底做了什么事情?
源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| type call struct { wg sync.WaitGroup val interface{} err error }
type Group struct { mu sync.Mutex m map[string]*call }
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } if c, ok := g.m[key]; ok { c.dups++ g.mu.Unlock() c.wg.Wait() return c.val, c.err, true } c := new(call) c.wg.Add(1) g.m[key] = c g.mu.Unlock()
g.doCall(c, key, fn) return c.val, c.err, c.dups > 0 }
|
go 的代码一直都很清晰易懂,可以看到先定义了结构体group和call,group.mu是保护group.m的互斥锁,group.m主要是保存请求的key,而call结构体是用来记录回调函数的结果。
在Do函数中,函数先是判断这个 key 是否是第一次调用,如果是,就会进入doCall调用回调函数获取结果,后续的请求就会阻塞在c.wg.Wait()这里,等待回调函数返回以后,直接拿到结果。
singleflight 的应用
所以依靠 singleflight ,针对并发缓存的更新,我们就可以这样实现:
1 2 3 4 5 6
| data = getCache(key) if !data { data = g.Do(key, func(){ return selectDB(key) }) }
|
Thanks
使用 singleflight 代替传统的并发锁
Golang singleflight 用武之地