Golang 并发编程 SingleFlight模式


Posted in Golang onApril 26, 2022

最近接触到微服务框架go-zero,翻看了整个框架代码,发现结构清晰、代码简洁,所以决定阅读源码学习下,本次阅读的源码位于core/syncx/singleflight.go

go-zeroSingleFlight的作用是:将并发请求合并成一个请求,以减少对下层服务的压力。

应用场景

  • 查询缓存时,合并请求,提升服务性能。 假设有一个 IP 查询的服务,每次用户请求先在缓存中查询一个 IP 的归属地,如果缓存中有结果则直接返回,不存在则进行 IP 解析操作。

Golang 并发编程 SingleFlight模式

如上图所示,n 个用户请求查询同一个 IP(8.8.8.8)就会对应 n 个 Redis 的查询,在高并发场景下,如果能将 n 个 Redis 查询合并成一个 Redis 查询,那么性能肯定会提升很多,而 SingleFlight就是用来实现请求合并的,效果如下:

Golang 并发编程 SingleFlight模式

  • 防止缓存击穿。

缓存击穿问题是指:在高并发的场景中,大量的请求同时查询一个 key ,如果这个 key 正好过期失效了,就会导致大量的请求都打到数据库,导致数据库的连接增多,负载上升。

Golang 并发编程 SingleFlight模式

通过SingleFlight可以将对同一个Key的并发请求进行合并,只让其中一个请求到数据库进行查询,其他请求共享同一个结果,可以很大程度提升并发能力。

应用方式

直接上代码:

func main() {
  round := 10
  var wg sync.WaitGroup
  barrier := syncx.NewSingleFlight()
  wg.Add(round)
  for i := 0; i < round; i++ {
    go func() {
      defer wg.Done()
      // 启用10个协程模拟获取缓存操作
      val, err := barrier.Do("get_rand_int", func() (interface{}, error) {
        time.Sleep(time.Second)
        return rand.Int(), nil
      })
      if err != nil {
        fmt.Println(err)
      } else {
        fmt.Println(val)
      }
    }()
  }
  wg.Wait()
}

以上代码,模拟 10 个协程请求 Redis 获取一个 key 的内容,代码很简单,就是执行Do()方法。其中,接收两个参数,第一个参数是获取资源的标识,可以是 redis 中缓存的 key,第二个参数就是一个匿名函数,封装好要做的业务逻辑。最终获得的结果如下:

5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410

从上看出,10个协程都获得了同一个结果,也就是只有一个协程真正执行了rand.Int()获取了随机数,其他的协程都共享了这个结果。

源码解析

先看代码结构:

type (
  // 定义接口,有2个方法 Do 和 DoEx,其实逻辑是一样的,DoEx 多了一个标识,主要看Do的逻辑就够了
  SingleFlight interface {
    Do(key string, fn func() (interface{}, error)) (interface{}, error)
    DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
  }
  // 定义 call 的结构
  call struct {
    wg  sync.WaitGroup // 用于实现通过1个 call,其他 call 阻塞
    val interface{}    // 表示 call 操作的返回结果
    err error          // 表示 call 操作发生的错误
  }
  // 总控结构,实现 SingleFlight 接口
  flightGroup struct {
    calls map[string]*call // 不同的 call 对应不同的 key
    lock  sync.Mutex       // 利用锁控制请求
  }
)

然后看最核心的Do方法做了什么事情:

func (g *flightGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
  c, done := g.createCall(key)
  if done {
    return c.val, c.err
  }
  g.makeCall(c, key, fn)
  return c.val, c.err
}

代码很简洁,利用g.createCall(key)对 key 发起 call 请求(其实就是做一件事情),如果此时已经有其他协程已经在发起 call 请求就阻塞住(done 为 true 的情况),等待拿到结果后直接返回。如果 done 是 false,说明当前协程是第一个发起 call 的协程,那么就执行g.makeCall(c, key, fn)真正地发起 call 请求(此后的其他协程就阻塞在了g.createCall(key))。 

Golang 并发编程 SingleFlight模式

从上图可知,其实关键就两步:

  • 判断是第一个请求的协程(利用map)
  • 阻塞住其他所有协程(利用 sync.WaitGroup)

来看下g.createCall(key)如何实现的:

func (g *flightGroup) createCall(key string) (c *call, done bool) {
  g.lock.Lock()
  if c, ok := g.calls[key]; ok {
    g.lock.Unlock()
    c.wg.Wait()
    return c, true
  }
  c = new(call)
  c.wg.Add(1)
  g.calls[key] = c
  g.lock.Unlock()
  return c, false
}

先看第一步:判断是第一个请求的协程(利用map)

g.lock.Lock()
if c, ok := g.calls[key]; ok {
  g.lock.Unlock()
  c.wg.Wait()
  return c, true
}

此处判断 map 中的 key 是否存在,如果已经存在,说明已经有其他协程在请求了,当前这个协程只需要等待,等待是利用了sync.WaitGroupWait()方法实现的,此处还是很巧妙的。要注意的是,map 在 Go 中是非并发安全的,所以需要加锁。

再看第二步:阻塞住其他所有协程(利用 sync.WaitGroup)

c = new(call)
c.wg.Add(1)
g.calls[key] = c

因为是第一个发起 call 的协程,所以需要 new 这个 call,然后将wg.Add(1),这样就对应了上面的wg.Wait(),阻塞剩下的协程。随后将 new 的 call 放入 map 中,注意此时只是完成了初始化,并没有真正去执行call请求,真正的处理逻辑在 g.makeCall(c, key, fn)中。

func (g *flightGroup) makeCall(c *call, key string, fn func() (interface{}, error)) {
  defer func() {
    g.lock.Lock()
    delete(g.calls, key)
    g.lock.Unlock()
    c.wg.Done()
  }()
  c.val, c.err = fn()
}

这个方法中做的事情很简单,就是执行了传递的匿名函数fn()(也就是真正call请求要做的事情)。最后处理收尾的事情(通过defer),也是分成两步:

  • 删除 map 中的 key,使得下次发起请求可以获取新的值。
  • 调用wg.Done(),让之前阻塞的协程全部获得结果并返回。

至此,SingleFlight 的核心代码就解析完毕了,虽然代码不长,但是这个思想还是很棒的,可以在实际工作中借鉴。

总结

  • map 非并发安全,记得加锁。
  • 巧用 sync.WaitGroup 去完成需要阻塞控制协程的应用场景。
  • 通过匿名函数 fn 去封装传递具体业务逻辑,在调用 fn 的上层函数中去完成统一的逻辑处理。

项目地址

https://github.com/zeromicro/go-zero

以上就是SingleFlight模式的Go并发编程学习的详细内容!


Tags in this post...

Golang 相关文章推荐
Go语言-为什么返回值为接口类型,却返回结构体
Apr 24 Golang
golang正则之命名分组方式
Apr 25 Golang
Golang 使用Map实现去重与set的功能操作
Apr 29 Golang
完美解决golang go get私有仓库的问题
May 05 Golang
基于Golang 高并发问题的解决方案
May 08 Golang
聊聊golang中多个defer的执行顺序
May 08 Golang
Golang二维数组的使用方式
May 28 Golang
详解Go语言Slice作为函数参数的使用
Jul 02 Golang
入门学习Go的基本语法
Jul 07 Golang
K8s部署发布Golang应用程序的实现方法
Jul 16 Golang
Golang Elasticsearches 批量修改查询及发送MQ
Apr 19 Golang
详解Go语言中Get/Post请求测试
Jun 01 Golang
Golang 实现 WebSockets 之创建 WebSockets
Apr 24 #Golang
Golang 实现WebSockets
Golang ort 中的sortInts 方法
Apr 24 #Golang
Golang 切片(Slice)实现增删改查
Apr 22 #Golang
Golang 结构体数据集合
Apr 22 #Golang
Golang map映射的用法
Apr 22 #Golang
Golang bufio详细讲解
Apr 21 #Golang
You might like
牡丹941资料
2021/03/01 无线电
实用函数10
2007/11/08 PHP
php实现的美国50个州选择列表实例
2015/04/20 PHP
PHP实现批量删除(封装)
2017/04/28 PHP
php中Ioc(控制反转)和Di(依赖注入)
2017/05/07 PHP
thinkPHP通用控制器实现方法示例
2017/11/23 PHP
[原创]保存的js无法执行的解决办法
2007/02/25 Javascript
JS 中document.URL 和 windows.location.href 的区别
2009/11/11 Javascript
页面元素绑定jquery toggle后元素隐藏的解决方法
2014/03/27 Javascript
用js的document.write输出的广告无阻塞加载的方法
2014/06/05 Javascript
如何正确使用Nodejs 的 c++ module 链接到 OpenSSL
2014/08/03 NodeJs
jQuery插件jRumble实现网页元素抖动
2015/06/05 Javascript
实例解析jQuery工具函数
2016/12/01 Javascript
用angular实现多选按钮的全选与反选实例代码
2017/05/23 Javascript
jQuery实现简单日期格式化功能示例
2017/09/19 jQuery
解决mpvue + vuex 开发微信小程序vuex辅助函数mapState、mapGetters不可用问题
2018/08/03 Javascript
vue-cli项目修改文件热重载失效的解决方法
2018/09/19 Javascript
Vue在 Nuxt.js 中重定向 404 页面的方法
2019/04/23 Javascript
原理深度解析Vue的响应式更新比React快
2020/04/04 Javascript
angular组件间传值测试的方法详解
2020/05/07 Javascript
[01:41]DOTA2 2015国际邀请赛中国区预选赛第三日战报
2015/05/28 DOTA
[01:01:52]DOTA2-DPC中国联赛正赛 iG vs LBZS BO3 第一场 3月4日
2021/03/11 DOTA
python简单实现基于SSL的IRC bot实例
2015/06/15 Python
python中pygame针对游戏窗口的显示方法实例分析(附源码)
2015/11/11 Python
Python中int()函数的用法浅析
2017/10/17 Python
python opencv实现运动检测
2018/07/10 Python
python 通过麦克风录音 生成wav文件的方法
2019/01/09 Python
浅谈pycharm使用及设置方法
2019/09/09 Python
pycharm中import呈现灰色原因的解决方法
2020/03/04 Python
CSS3教程(10):CSS3 HSL声明设置颜色
2009/04/02 HTML / CSS
日本最新流行服饰网购:Nissen
2016/07/24 全球购物
三个Unix的命令面试题
2015/04/12 面试题
2014年政风行风工作总结
2014/11/22 职场文书
应届毕业生求职简历自我评价
2015/03/02 职场文书
保安辞职申请书应该怎么写?
2019/07/15 职场文书
golang中切片copy复制和等号复制的区别介绍
2021/04/27 Golang