Go扩展原语之SingleFlight的用法详解

 更新时间:2023年07月26日 10:03:02   作者:码一行  
Go语言扩展包同步原语singleflight.Group能够再一个服务中抑制对下游的多次重复请求,它能够限制对同一个键值对的多次重复请求,减少对下游的瞬时流量,接下来小编就给大家讲讲Go SingleFlight的具体用法,需要的朋友可以参考下

概述

singleflight.Group 是 Go 语言扩展包的另一种同步原语,它能够再一个服务中抑制对下游的多次重复请求。一个比较常见的使用场景是,我们使用 Redis 对数据库中的数据进行缓存,发生缓存击穿时,大量请求会打到数据库上进而影响服务的尾延时。

singleflight.Group 能够有效地解决这个问题,它能够限制对同一个键值对的多次重复请求,减少对下游的瞬时流量。

在资源的获取非常昂贵时(例如访问缓存、数据库),就很适合使用 singleflight.Group 优化服务。它的使用方法如下:

type service struct {
    requestGroup singleflight.Group
}
func (s *service) handleRequest(ctx context.Context, request Request) (Response, error) {
    v, err, _ := requestGroup.Do(request.Hash(), func() (interface{}, error) {
        rows, err := // select * from tables
        if err != nil {
            return nil, err
        }
        return rows, nil
    })
    if err != nil {
        return nil, err
    }
    return Response{
        rows: rows,
    }, nil
}

因为请求的哈希在业务上一般表示相同的请求,所以上述代码使用它作为请求的键。当然,我们也可以选择其他的字段作为 singleflight.Group.Do 方法的第一个参数减少重复的请求。

结构体

singleflight.Group 结构体由一个互斥锁 sync.Mutex 和一个映射表组成,每一个 singleflight.call 结构体都保存了当前调用对应的信息:

type Group struct {
	mu sync.Mutex
	m  map[string]*call
}
type call struct {
	wg sync.WaitGroup
	val interface{}
	err error
	dups  int
	chans []chan<- Result
}

singleflight.call 结构体中的 val 和 err 字段都只会在执行传入的函数时赋值一次并在 sync.WaitGroup.Wait 返回时被读取。

dups 和 chans 两个字段分别存储了抑制的请求数量以及用于同步结果的 Channel。

接口

singleflight.Group求的方法:

  • singleflight.Group.Do  — 同步等待的方法;
  • singleflight.Group.DoChan — 返回 Channel 异步等待的方法;

这两个方法在功能上没有太多的区别,只是在接口的表现上稍有不同。

每次调用 singleflight.Group.Do 方法时都会获取互斥锁,随后判断是否已经存在键对应的 singleflight.call

  • 当不存在对应的 singleflight.call 时:

    • 初始化一个新的 singleflight.call 指针
    • 增加 sync.WaitGroup 持有的计数器
    • 将 singleflight.call 指针添加到映射表
    • 释放持有的互斥锁
    • 阻塞地调用 singleflight.Group.doCall 方法等待结果的返回
  • 当存在对应的 singleflight.call 时:

    • 增加 dups 计数器,它表示当前重复的调用次数
    • 释放持有的互斥锁
    • 通过 sync.WaitGroup.Wait 等待请求的返回
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()
            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
}

因为 val 和 err 两个字段都只会在 singleflight.Group.doCall 方法中赋值,所以当 singleflight.Group.doCall 和 sync.WaitGroup.Wait 返回时,函数调用的结果和错误都会返回给 singleflight.Group.Do 的调用方。

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
	c.val, c.err = fn()
	c.wg.Done()
	g.mu.Lock()
	delete(g.m, key)
	for _, ch := range c.chans {
		ch <- Result{c.val, c.err, c.dups > 0}
	}
	g.mu.Unlock()
}
  • 行传入的函数 fn,该函数的返回值会赋值给 c.val 和 c.err
  • 调用 sync.WaitGroup.Done 方法通知所有等待结果的 Goroutine — 当前函数已经执行完成,可以从 call 结构体中取出返回值并返回了
  • 获取持有的互斥锁并通过管道将信息同步给使用 singleflight.Group.DoChan 方法的 Goroutine
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
}

singleflight.Group.Do 和 singleflight.Group.DoChan 分别提供了同步和异步的调用方式,这让我们使用起来也更加灵活。

小结

当我们需要减少对下游的相同请求时,可以使用 singleflight.Group 来增加吞吐量和服务质量,不过在使用的过程中我们也需要注意以下的几个问题:

  • singleflight.Group.Do 和 singleflight.Group.DoChan 一个用于同步阻塞调用传入的函数,一个用于异步调用传入的参数并通过 Channel 接收函数的返回值
  • singleflight.Group.Forget 可以通知 singleflight.Group 在持有的映射表中删除某个键,接下来对该键的调用就不会等待前面的函数返回了
  • 一旦调用的函数返回了错误,所有在等待的 Goroutine 也都会接收到同样的错误

到此这篇关于Go扩展原语之SingleFlight的用法详解的文章就介绍到这了,更多相关Go SingleFlight内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • GoLang中Module的基本使用方法

    GoLang中Module的基本使用方法

    Go module是从Go 1.11版本才引入的新功能,下面这篇文章主要给大家介绍了关于GoLang中Module的基本使用方法,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • golang操作Redis的实现示例

    golang操作Redis的实现示例

    本文主要介绍了golang操作Redis的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-04-04
  • Golang性能提升利器之SectionReader的用法详解

    Golang性能提升利器之SectionReader的用法详解

    本文将介绍 Go 语言中的 SectionReader,包括 SectionReader的基本使用方法、实现原理、使用注意事项,感兴趣的小伙伴可以了解一下
    2023-07-07
  • golang调用shell命令(实时输出,终止)

    golang调用shell命令(实时输出,终止)

    本文主要介绍了golang调用shell命令(实时输出,终止),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • Systemd集成Golang二进制程序的方法

    Systemd集成Golang二进制程序的方法

    这篇文章主要介绍了Systemd集成Golang二进制程序的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-10-10
  • Golang二进制文件混淆保护操作

    Golang二进制文件混淆保护操作

    这篇文章主要介绍了Golang二进制文件混淆保护操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • VsCode下开发Go语言的环境配置超详细图文详解

    VsCode下开发Go语言的环境配置超详细图文详解

    vscode是一款跨平台、轻量级、插件多的开源IDE,在vscode不仅可以配置C/C++、Python、R、Ruby等语言的环境,还可以配置Go语言的环境,下面这篇文章主要给大家介绍了关于VsCode下开发Go语言的环境配置,需要的朋友可以参考下
    2024-03-03
  • go语言中函数的用法示例详解

    go语言中函数的用法示例详解

    Go语言中函数是基本的代码组织单元,用于封装一段代码,使代码结构更清晰、可复用,本文详细讲解了基本函数定义、参数传递、返回值、多返回值、匿名函数、递归和defer语句的使用,感兴趣的朋友一起看看吧
    2024-10-10
  • 一文带你了解Golang中reflect反射的常见错误

    一文带你了解Golang中reflect反射的常见错误

    go 反射的错误大多数都来自于调用了一个不适合当前类型的方法, 而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会 panic。本文就介绍一下使用 go 反射时很大概率会出现的错误,需要的可以参考一下
    2023-01-01
  • beego获取ajax数据的实例

    beego获取ajax数据的实例

    下面小编就为大家分享一篇beego获取ajax数据的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12

最新评论