Go并发原语之SingleFlight请求合并方法实例

 更新时间:2023年12月14日 10:42:48   作者:fliyu  
本文我们来学习一下 Go 语言的扩展并发原语:SingleFlight,SingleFlight 的作用是将并发请求合并成一个请求,以减少重复的进程来优化 Go 代码

SingleFlight 的使用场景

在处理多个 goroutine 同时调用同一个函数的时候,如何只用一个 goroutine 去调用一次函数,并将返回结果给到所有 goroutine,这是可以使用 SingleFlight,可以减少并发调用的数量。

在高并发请求场景中,例如秒杀场景:多个用户在同一时间查询库存数,这时候对于所有的用户而言,同一时间查询结果都是一样的,如果后台都去查缓存或者数据库,那么性能压力很大。如果相同时间只有一个查询,那么性能将显著提升。

一句话总结:SingleFlight 主要作用是合并并发请求的场景,针对于相同的读请求。

SingleFlight 的基本使用

下面先看看这段代码,5个协程同时并发返回 getProductById ,看看输出结果如何:

func main() {
  var wg sync.WaitGroup
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      result := getProductById("商品A")
      fmt.Printf("%v\n", result)
    }()
  }
  wg.Wait()
}
func getProductById(name string) string {
  fmt.Println("getProductById doing...")
  time.Sleep(time.Millisecond * 10) // 模拟一下耗时
  return name
}
$ go run main.go
getProductById doing...
getProductById doing...
getProductById doing...
getProductById doing...
getProductById doing...
商品A
商品A
商品A
商品A
商品A

可以看出 getProductById 方法被访问了五次,那么如何通过 SingleFlight 进行优化呢?

定义一个全局变量 SingleFlight,在访问 getProductById 方法时调用 Do 方法,即可实现同一时间只有一次方法,代码如下:

import (
  "fmt"
  "golang.org/x/sync/singleflight"
  "sync"
  "time"
)
var g singleflight.Group
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      resp, _, _ := g.Do("商品A", func() (interface{}, error) {
        result := getProductById("商品A")
        return result, nil
      })
      fmt.Printf("%v\n", resp)
    }()
  }
  wg.Wait()
}
func getProductById(name string) string {
  fmt.Println("getProductById doing...")
  time.Sleep(time.Millisecond * 10) // 模拟一下耗时
  return name
}
$ go run main.go
getProductById doing...
商品A
商品A
商品A
商品A
商品A

你可能会想 SingleFlight 和 sync.Once 的区别,sync.Once 主要是用在单次初始化场景中,而 SingleFlight 主要用在合并请求中,针对于同一时间的并发场景。

SingleFlight 的实现原理

SingleFlight 的数据结构是 Group ,结构如下:

// call is an in-flight or completed singleflight.Do call
type call struct {
  wg sync.WaitGroup
  // These fields are written once before the WaitGroup is done
  // and are only read after the WaitGroup is done.
  val interface{}
  err error
  // These fields are read and written with the singleflight
  // mutex held before the WaitGroup is done, and are read but
  // not written after the WaitGroup is done.
  dups  int
  chans []chan<- Result
}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
  mu sync.Mutex       // protects m
  m  map[string]*call // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
  Val    interface{}
  Err    error
  Shared bool
}

可以看出,SingleFlight 是使用互斥锁 Mutex 和 Map 来实现的。互斥锁 Mutex 提供并发时的读写保护,而 Map 用于保存同一个 key 正在处理的请求。

其提供了3个方法:

Do 方法的实现逻辑

// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
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()
    if e, ok := c.err.(*panicError); ok {
      panic(e)
    } else if c.err == errGoexit {
      runtime.Goexit()
    }
    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
}

SingleFlight 定义了一个辅助对象 call,用于代表正在执行 fn 函数的请求或者是否已经执行完请求。

  • 如果存在相同的 key,其他请求将会等待这个 key 执行完成,并使用第一个 key 获取到的请求结果
  • 如果不存在,创建一个 call ,并将其加入到 map 中,执行调用 fn 函数。

DoChan 方法的实现逻辑

而 DoChan 方法与 Do 方法类似:

// DoChan is like Do but returns a channel that will receive the
// results when they are ready.
//
// The returned channel will not be closed.
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
}

Forget 方法的实现逻辑

// Forget tells the singleflight to forget about a key.  Future calls
// to Do for this key will call the function rather than waiting for
// an earlier call to complete.
func (g *Group) Forget(key string) {
  g.mu.Lock()
  delete(g.m, key)
  g.mu.Unlock()
}

将 key 从 map 中删除。

总结

使用 SingleFlight 时,通过将多个请求合并成一个,降低并发访问的压力,极大地提升了系统性能,针对于多并发读请求的场景,可以考虑是否满足 SingleFlight 的使用情况。

而对于并发写请求的场景,如果是多次写只需要一次的情况,那么也是满足的。例如:每个 http 请求都会携带 token,每次请求都需要把 token 存入缓存或者写入数据库,如果多次并发请求同时来,只需要写一次即可

以上就是Go并发原语之SingleFlight请求合并方法实例的详细内容,更多关于Go SingleFlight 请求合并的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言Zap日志库使用教程

    Go语言Zap日志库使用教程

    在项目开发中,经常需要把程序运行过程中各种信息记录下来,有了详细的日志有助于问题排查和功能优化;但如何选择和使用性能好功能强大的日志库,这个就需要我们从多角度考虑
    2023-02-02
  • Go 连接 MySQL之 MySQL 预处理详解

    Go 连接 MySQL之 MySQL 预处理详解

    Go语言提供了丰富的库和工具,可以方便地连接MySQL数据库。MySQL预处理是一种提高数据库操作效率和安全性的技术。Go语言中的第三方库提供了MySQL预处理的支持,通过使用预处理语句,可以避免SQL注入攻击,并且可以提高数据库操作的效率。
    2023-06-06
  • Windows下升级go版本过程详解

    Windows下升级go版本过程详解

    这篇文章主要为大家介绍了Windows下升级go版本过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • 基于Go语言实现压缩文件处理

    基于Go语言实现压缩文件处理

    在现代的应用开发中,处理压缩文件(如 .zip 格式)是常见的需求,本文将介绍如何使用 Go 语言封装一个 ziputil 包,来处理文件的压缩和解压操作,需要的可以了解下
    2024-11-11
  • 如何在Golang中运行JavaScript

    如何在Golang中运行JavaScript

    最近写一个程序,接口返回的数据是js格式的,需要通过golang来解析js,所以下面这篇文章主要给大家介绍了关于如何在Golang中运行JavaScript的相关资料,需要的朋友可以参考下
    2022-01-01
  • 使用golang实现PDF图片提取

    使用golang实现PDF图片提取

    这篇文章主要为大家详细介绍了如何使用golang实现PDF图片提取功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-11-11
  • Golang的select多路复用及channel使用操作

    Golang的select多路复用及channel使用操作

    这篇文章主要介绍了Golang的select多路复用及channel使用操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go语言TCP从原理到代码实现详解

    Go语言TCP从原理到代码实现详解

    这篇文章主要为大家介绍了Go语言TCP从原理到代码实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Golang的strings.Split()踩坑记录

    Golang的strings.Split()踩坑记录

    工作中,当我们需要对字符串按照某个字符串切分成字符串数组数时,常用到strings.Split(),本文主要介绍了Golang的strings.Split()踩坑记录,感兴趣的可以了解一下
    2022-05-05
  • 深入解析Go语言中crypto/subtle加密库

    深入解析Go语言中crypto/subtle加密库

    本文主要介绍了深入解析Go语言中crypto/subtle加密库,详细介绍crypto/subtle加密库主要函数的用途、工作原理及实际应用,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02

最新评论