聊两句-singleflight

209 阅读2分钟

最近看一段数据获取的代码,看使用到了singleflight,代码很简短,但是能帮助实现防止缓存击穿的情况,简单记录一下。

防缓存击穿

我们在开发时,有时会碰到一个接口的访问量突然上升,导致服务响应延迟或者宕机的情况。一个常用的操作是为了防止高并发的访问数据库的操作,一般会加一些缓存的操作,来缓解数据库的压力。 不过有一些异常情况,比如在缓存失效重新获取的过程中 同一个时间点有大量并发的访问同一个数据的请求, 这个时候因为缓存还没有刷新,会有很多的请求访问数据库,导致数据库压力过大。 singleflight通过把相同的请求进行汇总,发现已经有请求去访问的话,就把其他的数据访问hold住,等待某一个请求回来再统一返回。

具体代码分析

对外提供了两套接口,一个是直接返回结果的接口Do,一个是返回了统计信息的接口DoChan。

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}

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {   ch := make(chan Result, 1)   g.mu.Lock()   if g.m == nil {      g.m = make(map[string]*call)   }   if c, ok := g.m[key]; ok {      c.dups++      c.chans = append(c.chans, ch)      g.mu.Unlock()      return ch   }   c := &call{chans: []chan<- Result{ch}}   c.wg.Add(1)   g.m[key] = c   g.mu.Unlock()   go g.doCall(c, key, fn)   return ch}

以Do接口为例。

先使用mutext的的锁,确保代码没有同步的问题。 

if c, ok := g.m[key]; ok {   //判断是否已经有执行的代码

如果已经有的话,通过sync.WaitGroup的wait方法来进行hold住逻辑,等待第一个执行逻辑ok以后,返回相应的值。

如果没有的话,会创建一个call的结构,sync.WaitGroup方法会执行相应的Add操作,并把操作的key加入到map中。解锁,执行相应的函数。

关于doCall的方式:

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {   c.val, c.err = fn()   c.wg.Done()   g.mu.Lock()   delete(g.m, key)   for _, ch := range c.chans {      ch <- Result{c.val, c.err, c.dups > 0}   }   g.mu.Unlock()}

doCall执行传入的函数,并且会遍历为每个访问逻辑生成的chan,并把统计结果通过chan来返回。

简单聊两句:

singleflight总体就100多行代码,解决的问题也比较的明确,非常的短小精悍。对于返回值来说,第一种方式,直接返回结果,这个非常的明确。第二种方式是会返回一个chan的方式,并传递一些参数。 以前写iOS代码的时候,对于异步的返回,一般是传递一个block进去,然后完成的时候通过block来异步回调,或者通过通知的方式来进行解耦。 这次又会多一种异步同步的方式,说不定可以借鉴,挺好的。