缓存更新问题
缓存是高并发业务的基石,当访问量突然上升的时候,缓存失效回源时会将请求打到后台数据库,导致服务器响应延迟或者宕机的情况。
通常缓存更新方案:
- 1.业务代码中,根据key从缓存拿不到数据,访问存储层获取数据后更新缓存
- 2.由专门的定时脚本在缓存失效前对其进行更新
- 3.通过分布式锁,实现只有一个请求负责缓存更新,其他请求等待:一种基于哨兵的缓存访问策略
通常获取缓存这样写:
| 12
 3
 4
 
 | data = getCache(key)if !data {
 data = selectDB(key)
 }
 
 | 
并发获取
当缓存失效,集中查询DB时,此时需要考虑加锁。
加锁
| 12
 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 :
| 12
 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函数到底做了什么事情?
源码分析
| 12
 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 ,针对并发缓存的更新,我们就可以这样实现:
| 12
 3
 4
 5
 6
 
 | data = getCache(key)if !data {
 data = g.Do(key, func(){
 return selectDB(key)
 })
 }
 
 | 
Thanks
使用 singleflight 代替传统的并发锁
Golang singleflight 用武之地