最近看一段数据获取的代码,看使用到了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来异步回调,或者通过通知的方式来进行解耦。 这次又会多一种异步同步的方式,说不定可以借鉴,挺好的。