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:垃圾回收GC触发条件详解
Apr 24 Golang
go语言-在mac下brew升级golang
Apr 25 Golang
Golang 正则匹配效率详解
Apr 25 Golang
Golang二维数组的使用方式
May 28 Golang
修改并编译golang源码的操作步骤
Jul 25 Golang
Go 通过结构struct实现接口interface的问题
Oct 05 Golang
Golang 并发下的问题定位及解决方案
Mar 16 Golang
victoriaMetrics库布隆过滤器初始化及使用详解
Apr 05 Golang
golang使用map实现去除重复数组
Apr 14 Golang
GO语言字符串处理函数之处理Strings包
Apr 14 Golang
Golang日志包的使用
Apr 20 Golang
go goth封装第三方认证库示例详解
Aug 14 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
用PHP程序实现支持页面后退的两种方法
2008/06/30 PHP
php5 apache 2.2 webservice 创建与配置(java)
2011/01/27 PHP
php第一次无法获取cookie问题处理
2014/12/15 PHP
php桥接模式应用案例分析
2019/10/23 PHP
javascript 支持链式调用的异步调用框架Async.Operation
2009/08/04 Javascript
SWFObject 2.1以上版本语法介绍
2010/07/10 Javascript
javascript学习笔记(二十) 获得和设置元素的特性(属性)
2012/06/20 Javascript
JS实现倒计时和文字滚动的效果实例
2014/10/29 Javascript
jQuery选择id属性带有点符号元素的方法
2015/03/17 Javascript
ajax如何实现页面局部跳转与结果返回
2015/08/24 Javascript
JS+CSS实现六级网站导航主菜单效果
2015/09/28 Javascript
JavaScript测试工具之Karma-Jasmine的安装和使用详解
2015/12/03 Javascript
JavaScript基于原型链的继承
2016/06/22 Javascript
node.js的事件机制
2017/02/08 Javascript
微信小程序左滑删除效果的实现代码
2017/02/20 Javascript
vue组件间通信解析
2017/03/01 Javascript
微信小程序实战之轮播图(3)
2017/04/17 Javascript
原生JavaScript实现精美的淘宝轮播图效果示例【附demo源码下载】
2017/05/27 Javascript
node中Express 动态设置端口的方法
2017/08/04 Javascript
使用vue-resource进行数据交互的实例
2017/09/02 Javascript
JS+HTML+CSS实现轮播效果
2017/11/28 Javascript
Vue2.0用户权限控制解决方案
2017/11/29 Javascript
详解Vue项目编译后部署在非网站根目录的解决方案
2018/04/26 Javascript
[01:56]《DOTA2》中文配音CG
2013/04/22 DOTA
Django的分页器实例(paginator)
2017/12/01 Python
python3.6使用pickle序列化class的方法
2018/10/22 Python
python+django+rest框架配置创建方法
2019/08/31 Python
python openvc 裁剪、剪切图片 提取图片的行和列
2019/09/19 Python
python实现的按要求生成手机号功能示例
2019/10/08 Python
日本著名的平价时尚女性购物网站:Fifth
2016/08/24 全球购物
地球上最先进的胡子和头发修剪器:Bevel
2018/01/23 全球购物
亚马逊墨西哥站:Amazon.com.mx
2018/08/26 全球购物
经典婚礼主持词
2014/03/13 职场文书
基本公共卫生服务健康教育工作方案
2014/05/22 职场文书
浅谈mysql返回Boolean类型的几种情况
2021/06/04 MySQL
windows10 家庭版下FTP服务器搭建教程
2022/08/05 Servers