侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

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

Go  /  管理员 发布于 7年前   348

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

缓存击穿:一个存在的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和对应的callc := 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防止缓存击穿内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持! 


  • 上一条:
    VSCode1.4 搭建Golang的开发调试环境(遇到很多问题)
    下一条:
    GoLang之使用Context控制请求超时的实现
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 近期文章
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2017-09
    • 2020-03
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-12
    • 2021-01
    • 2021-05
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-10
    • 2021-11
    • 2021-12
    • 2022-01
    • 2022-02
    • 2022-03
    • 2022-04
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-08
    • 2023-09
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-02
    • 2024-03
    • 2024-04
    • 2024-05
    • 2024-06
    • 2024-07
    • 2024-08
    • 2024-11
    • 2025-02
    • 2025-04
    • 2025-05
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客