使用Golang的singleflight防止缓存击穿的方法

 更新时间:2020年04月13日 15:14:30   作者:Chaaaaaaaaauncey  
这篇文章主要介绍了使用Golang的singleflight防止缓存击穿的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

在使用缓存时,容易发生缓存击穿。

缓存击穿:一个存在的key,在缓存过期的瞬间,同时有大量的请求过来,造成所有请求都去读dB,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

singleflight

介绍

import "golang.org/x/sync/singleflight"

singleflight类的使用方法就新建一个singleflight.Group,使用其方法Do或者DoChan来包装方法,被包装的方法在对于同一个key,只会有一个协程执行,其他协程等待那个协程执行结束后,拿到同样的结果。

Group结构体

代表一类工作,同一个group中,同样的key同时只能被执行一次。

Do方法

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)

key:同一个key,同时只有一个协程执行。

fn:被包装的函数。

v:返回值,即执行的结果。其他等待的协程都会拿到。

shared:表示是否有其他协程得到了这个结果v。

DoChan方法

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result

与Do方法一样,只是返回的是一个channel,执行结果会发送到channel中,其他等待的协程都可以从channel中拿到结果。

ref:https://godoc.org/golang.org/x/sync/singleflight

示例

使用Do方法来模拟,解决缓存击穿的问题

func main() {

  var singleSetCache singleflight.Group

  getAndSetCache:=func (requestID int,cacheKey string) (string, error) {

  log.Printf("request %v start to get and set cache...",requestID)

  value,_, _ :=singleSetCache.Do(cacheKey, func() (ret interface{}, err error) {//do的入参key,可以直接使用缓存的key,这样同一个缓存,只有一个协程会去读DB

    log.Printf("request %v is setting cache...",requestID)

     time.Sleep(3*time._Second_)

     log.Printf("request %v set cache success!",requestID)

    return "VALUE",nil

   })

  return value.(string),nil

  }

  cacheKey:="cacheKey"

  for i:=1;i<10;i++{//模拟多个协程同时请求

  go func(requestID int) {

     value,_:=getAndSetCache(requestID,cacheKey)

     log.Printf("request %v get value: %v",requestID,value)

   }(i)

  }

  time.Sleep(20*time._Second_)
}

输出:

2020/04/12 18:18:40 request 4 start  to  get  and  set cache...

2020/04/12 18:18:40 request 4 is setting cache...

2020/04/12 18:18:40 request 2 start  to  get  and  set cache...

2020/04/12 18:18:40 request 7 start  to  get  and  set cache...

2020/04/12 18:18:40 request 5 start  to  get  and  set cache...

2020/04/12 18:18:40 request 1 start  to  get  and  set cache...

2020/04/12 18:18:40 request 6 start  to  get  and  set cache...

2020/04/12 18:18:40 request 3 start  to  get  and  set cache...

2020/04/12 18:18:40 request 8 start  to  get  and  set cache...

2020/04/12 18:18:40 request 9 start  to  get  and  set cache...

2020/04/12 18:18:43 request 4 set  cache  success!

2020/04/12 18:18:43 request 4 get value: VALUE

2020/04/12 18:18:43 request 9 get value: VALUE

2020/04/12 18:18:43 request 6 get value: VALUE

2020/04/12 18:18:43 request 3 get value: VALUE

2020/04/12 18:18:43 request 8 get value: VALUE

2020/04/12 18:18:43 request 1 get value: VALUE

2020/04/12 18:18:43 request 5 get value: VALUE

2020/04/12 18:18:43 request 2 get value: VALUE

2020/04/12 18:18:43 request 7 get value: VALUE`

可以看到确实只有一个协程执行了被包装的函数,并且其他协程都拿到了结果。

源码分析

看一下这个Do方法是怎么实现的。

首先看一下Group的结构:

type Group struct {

  mu sync.Mutex   

  m map[string]*call //保存key对应的函数执行过程和结果的变量。

}

Group的结构非常简单,一个锁来保证并发安全,另一个map用来保存key对应的函数执行过程和结果的变量。

看下call的结构:

type call struct {

  wg sync.WaitGroup //用WaitGroup实现只有一个协程执行函数

  val interface{} //函数执行结果

  err error

  forgotten bool

  dups int //含义是duplications,即同时执行同一个key的协程数量

  chans []chan<- Result
}

看下Do方法

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {

  g.mu.Lock()//写Group的m字段时,加锁保证写安全。

  if g.m == nil {

  g.m = make(map[string]*call)

  }

if c, ok := g.m[key]; ok {//如果key已经存在,说明已经有协程在执行,则dups++,并等待其执行完毕后,返回其执行结果,执行结果保存在对应的call的val字段里

   c.dups++

   g.mu.Unlock()

   c.wg.Wait()

 return c.val, c.err, true

  }

//如果key不存在,则新建一个call,并使用WaitGroup来阻塞其他协程,同时在m字段里写入key和对应的call

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

}

继续看下g.doCall里具体干了什么

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {

  c.val, c.err = fn()//执行被包装的函数

  c.wg.Done()//执行完毕后,就可以通知其他协程可以拿结果了

  g.mu.Lock()

if !c.forgotten {//其实这里是为了保证执行完毕之后,对应的key被删除,Group有一个方法Forget(key string),可以用来主动删除key,这里是判断那个方法是否被调用过,被调用过则字段forgotten会置为true,如果没有被调用过,则在这里把key删除。

  delete(g.m, key)

  }

  for _, ch := range c.chans {//将执行结果发送到channel里,这里是给DoChan方法使用的

  ch <- Result{c.val, c.err, c.dups > 0}

  }

  g.mu.Unlock()

}

由此看来,其实现是非常简单的。不得不赞叹一百来行代码就实现了功能。

其他

顺便附上DoChan方法的使用示例:

func main() {

  var singleSetCache singleflight.Group

  getAndSetCache:=func (requestID int,cacheKey string) (string, error) {

  log.Printf("request %v start to get and set cache...",requestID)

  retChan:=singleSetCache.DoChan(cacheKey, func() (ret interface{}, err error) {

    log.Printf("request %v is setting cache...",requestID)

    time.Sleep(3*time._Second_)

    log.Printf("request %v set cache success!",requestID)

    return "VALUE",nil

   })

  var ret singleflight.Result

  timeout := time.After(5 * time._Second_)

  select {//加入了超时机制

    case <-timeout:

      log.Printf("time out!")

      return "",errors.New("time out")

    case ret =<- retChan://从chan中取出结果

      return ret.Val.(string),ret.Err

   }

  return "",nil

  }

  cacheKey:="cacheKey"
  
  for i:=1;i<10;i++{

  go func(requestID int) {

     value,_:=getAndSetCache(requestID,cacheKey)

     log.Printf("request %v get value: %v",requestID,value)

   }(i)

  }

  time.Sleep(20*time._Second_)

}

看下DoChan的源码

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)//可以看到,每个等待的协程,都有一个结果channel。从之前的g.doCall里也可以看到,每个channel都给塞了结果。为什么不所有协程共用一个channel?因为那样就得在channel里塞至少与协程数量一样的结果数量,但是你却无法保证用户一个协程只读取一次。

   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
}

到此这篇关于使用Golang的singleflight防止缓存击穿的方法的文章就介绍到这了,更多相关Golang singleflight防止缓存击穿内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Golang原生rpc(rpc服务端源码解读)

    Golang原生rpc(rpc服务端源码解读)

    本文主要介绍了Golang原生rpc(rpc服务端源码解读),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • 深入探究Golang中log标准库的使用

    深入探究Golang中log标准库的使用

    Go 语言标准库中的 log 包设计简洁明了,易于上手,可以轻松记录程序运行时的信息、调试错误以及跟踪代码执行过程中的问题等。本文主要来深入探究 log 包的使用和原理,帮助读者更好地了解和掌握它
    2023-05-05
  • golang 常用定时任务汇总

    golang 常用定时任务汇总

    这篇文章主要介绍了golang 常用定时任务汇总,golang利用goroutine外加github.com/gorhill/cronexpr库就可实现定时任务,具体代码介绍,需要的小伙伴可以参考一下
    2022-09-09
  • golang中channel+error来做异步错误处理有多香

    golang中channel+error来做异步错误处理有多香

    官方推荐golang中错误处理当做值处理, 既然是值那就可以在channel中传输,这篇文章主要介绍了golang 错误处理channel+error真的香,需要的朋友可以参考下
    2023-01-01
  • Go语言使用protojson库实现Protocol Buffers与JSON转换

    Go语言使用protojson库实现Protocol Buffers与JSON转换

    本文主要介绍Google开源的工具库Protojson库如何Protocol Buffers与JSON进行转换,以及和标准库encoding/json的性能对比,需要的朋友可以参考下
    2023-09-09
  • goland 设置project gopath的操作

    goland 设置project gopath的操作

    这篇文章主要介绍了goland 设置project gopath的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • go语言if/else语句简单用法示例

    go语言if/else语句简单用法示例

    这篇文章主要介绍了go语言if/else语句用法,结合实例形式分析了go语言if else语句的判定与流程控制技巧,需要的朋友可以参考下
    2016-05-05
  • Go常用技能日志log包创建使用示例

    Go常用技能日志log包创建使用示例

    这篇文章主要为大家介绍了Go常用技能日志log包创建使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • 利用go语言实现查找二叉树中的最大宽度

    利用go语言实现查找二叉树中的最大宽度

    这篇文章主要介绍了利用go语言实现查找二叉树中的最大宽度,文章围绕主题展开详细介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-05-05
  • go语言使用scp的方法实例分析

    go语言使用scp的方法实例分析

    这篇文章主要介绍了go语言使用scp的方法,实例分析了go语言调用scp命令的使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03

最新评论