首页 > 其他分享 >Go每日一库之187:singleflight(合并重复调用)

Go每日一库之187:singleflight(合并重复调用)

时间:2023-09-29 21:25:56浏览次数:31  
标签:调用 key err 一库 187 func Go singleflight getData

本文主要介绍Go语言中的singleflight包,包括什么是singleflight以及如何使用singleflight合并请求解决缓存击穿问题。

singleflight 目前(Go1.20)还属于Go的准标准库,它提供了重复函数调用抑制机制,使用它可以避免同时进行相同的函数调用。第一个调用未完成时后续的重复调用会等待,当第一个调用完成时则会与它们分享结果,这样以来虽然只执行了一次函数调用但是所有调用都拿到了最终的调用结果。

基础示例

我们首先来看以下示例代码,在第1次调用getData函数没返回结果时,再次调用getData函数。

package main

import (
	"fmt"
	"golang.org/x/sync/singleflight"
	"time"
)

func getData(id int64) string {
	fmt.Println("query...")
	time.Sleep(10 * time.Second) // 模拟一个比较耗时的操作
	return "liwenzhou.com"
}

func main() {
	g := new(singleflight.Group)

	// 第1次调用
	go func() {
		v1, _, shared := g.Do("getData", func() (interface{}, error) {
			ret := getData(1)
			return ret, nil
		})
		fmt.Printf("1st call: v1:%v, shared:%v\n", v1, shared)
	}()

	time.Sleep(2 * time.Second)

	// 第2次调用(第1次调用已开始但未结束)
	v2, _, shared := g.Do("getData", func() (interface{}, error) {
		ret := getData(1)
		return ret, nil
	})
	fmt.Printf("2nd call: v2:%v, shared:%v\n", v2, shared)
}

上述代码执行结果如下。

query...
1st call: v1:liwenzhou.com, shared:true
2nd call: v2:liwenzhou.com, shared:true

从输出可以看到getData函数只执行了一次(只打印了一次query...),但是两次调用都拿到了结果(liwenzhou.com)。

这就是singleflight包提供给我们的能力,避免了同时执行重复的函数。

singleflight介绍

singleflight包中定义了一个名为Group的结构体类型,它表示一类工作,并形成一个命名空间,在这个命名空间中,可以使用重复抑制来执行工作单元。

type Group struct {
	mu sync.Mutex       // 保护 m
	m  map[string]*call // 延迟初始化
}

Group类型有以下三个方法。

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)

Do 执行并返回给定函数的结果,确保一次只有一个给定key在执行。如果进入重复调用,重复调用方将等待原始调用方完成并会收到相同的结果。返回值shared表示是否给多个调用方赋值 v。

需要注意的是,使用Do方法时,如果第一次调用发生了阻塞,那么后续的调用也会发生阻塞。在极端场景下可能导致程序hang住。

singleflight包提供了DoChan方法,支持我们异步获取调用结果。

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result

DoChan 类似于 Do,但不是直接返回结果而是返回一个通道,该通道将在结果准备就绪时接收结果。返回的通道将不会关闭。

其中Result类型定义如下。

type Result struct {
	Val    interface{}
	Err    error
	Shared bool
}

Result 保存 Do 的结果,因此它们可以在通道上传递。

为了避免第一次调用阻塞所有调用的情况,我们可以结合使用select和DoChan为函数调用设置超时时间。

func doChanGetData(ctx context.Context, g *singleflight.Group, id int64) (string, error) {
	ch := g.DoChan("getData", func() (interface{}, error) {
		ret := getData(id)
		return ret, nil
	})
	select {
	case <-ctx.Done():
		return "", ctx.Err()
	case ret := <-ch:
		return ret.Val.(string), ret.Err
	}
}

func main() {
	g := new(singleflight.Group)

	// 第1次调用
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		defer cancel()
		v1, err := doChanGetData(ctx, g, 1)
		fmt.Printf("v1:%v err:%v\n", v1, err)
	}()

	time.Sleep(2 * time.Second)

	// 第2次调用(第1次调用已开始但未结束)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	v2, err := doChanGetData(ctx, g, 1)
	fmt.Printf("v2:%v err:%v\n", v2, err)
}

上述代码最终输出结果如下。

v1: err:context deadline exceeded
v2: err:context deadline exceeded

如果在某些场景下允许第一个调用失败后再次尝试调用该函数,而不希望同一时间内的多次请求都因第一个调用返回失败而失败,那么可以通过调用Forget方法来忘记这个key。

func (g *Group) Forget(key string)

Forget告诉singleflight忘记一个key。将来对这个key的 Do 调用将调用该函数,而不是等待以前的调用完成。

例如,可以在发起调用的同时,在另外的goroutine中延迟100ms调用Forget方法来忘记key。

func doGetData(g *singleflight.Group, id int64) (string, error) {
	v, err, _ := g.Do("getData", func() (interface{}, error) {
		go func() {
			time.Sleep(100 * time.Millisecond) // 100ms后忘记key
			g.Forget("getData")
		}()

		ret := getData(id)
		return ret, nil
	})
	return v.(string), err
}

应用场景

singleflight将并发调用合并成一个调用的特点决定了它非常适合用来防止缓存击穿。

下面是一段使用singleflight进行查询的伪代码。

func getDataSingleFlight(key string) (interface{}, error) {
	v, err, _ := g.Do(key, func() (interface{}, error) {
		// 查缓存
		data, err := getDataFromCache(key)
		if err == nil {
			return data, nil
		}
		if err == errNotFound {
			// 查DB
			data, err := getDataFromDB(key)
			if err == nil {
				setCache(data) // 设置缓存
				return data, nil
			}
			return nil, err
		}
		return nil, err // 缓存出错直接返回,防止灾难传递至DB
	})

	if err != nil {
		return nil, err
	}
	return v, nil
}

如下图所示,在查询数据时使用singleflight能够避免业务高峰期缓存失效导致大量请求直接打到DB的情况,从而提高系统的可用性。

总结

singleflight通过强制一个函数的所有后续调用等待第一个调用完成,消除了同时运行重复函数的低效性。与缓存不同,它只有在同时调用函数时才共享结果。它充当一个非常短暂的缓存,不需要手动作废或设置有效时间。

标签:调用,key,err,一库,187,func,Go,singleflight,getData
From: https://www.cnblogs.com/arena/p/17737377.html

相关文章

  • Go每日一库之186:sonic(高性能JSON库)
    介绍我们在日常开发中,常常会对JSON进行序列化和反序列化。Golang提供了encoding/json包对JSON进行Marshal/Unmarshal操作。但是在大规模数据场景下,该包的性能和开销确实会有点不够看。在生产环境下,JSON序列化和反序列化会被频繁的使用到。在测试中,CPU使用率接近10%,其中极端情况......
  • Go每日一库之184:katana(新一代爬虫框架)
    项目链接https://github.com/projectdiscovery/katana项目简介katana是一个使用golang编写的新一代爬虫框架,支持HTTP和headless抓取网页信息不仅可以作为库集成到Golang项目,还可以通过命令行直接抓取,对于有一些轻量级的抓取任务的开发者配合jq一起使用简直就是福......
  • Go每日一库之183:vegeta(http压力测试工具库)
    项目地址:https://github.com/tsenart/vegetahttps://mp.weixin.qq.com/s/J0PiqTifr_rs_S2CzMRoWg......
  • Go每日一库之182:RuleGo(轻量级高性能嵌入式规则引擎)
    ◆ 一、开源项目简介RuleGo是一个基于Go语言的轻量级、高性能、嵌入式的规则引擎。也一个灵活配置和高度定制化的事件处理框架。可以对输入消息进行过滤、转换、丰富和执行各种动作。◆ 二、开源协议使用Apache-2.0开源协议◆ 三、界面展示规则链规则链是规则节点及其关......
  • Go每日一库之181:conc(并发库)
    来自公司sourcegraph的conc**(https://github.com/sourcegraph/conc)并发库,目标是betterstructuredconcurrencyforgo,简单的评价一下每个公司都有类似的轮子,与以往的库比起来,多了泛型,代码写起来更优雅,不需要interface,不需要运行时assert,性能肯定更好我们在写通......
  • Go每日一库之180:fastcache(协程安全且支持大量数据存储的高性能缓存库)
    fastcache是一个线程安全并且支持大量数据存储的高性能缓存组件库。这是官方Github主页上的项目介绍,和fasthttp名字一样以fast打头,作者对项目代码的自信程度可见一斑。此外该库的核心代码非常轻量,笔者本着学习的目的分析下内部的代码实现。基准测试官方给出了fastca......
  • Go每日一库之179:env(将系统环境变量解析到结构体的库)
    该包的实现是基于标准库os/env包中的相关函数(比如Getenv)来获取系统的环境变量的。获取到环境变量值后,再通过结构体中的tag,将值映射到对应的结构体字段上。使用示例下面是将系统的一些环境变量映射到config结构体的示例。如下:我们可以像以下这样运行该代码:$PRODUCTION=trueHO......
  • Go每日一库之178:chromedp(一个基于Chrome DevTools协议的库,支持数据采集、截取网页长
    该库提供了一种简单、高效、可靠的方式来控制Chrome浏览器进行自动化测试和爬取数据。项目地址:https://github.com/chromedp/chromedp它可以模拟用户在浏览器中执行各种操作,如点击、输入文本、截取网页长图、将网页内容转换成pdf文档、下载图片等,从而获取到需要采集的数据。基......
  • Go每日一库之176:filetype(文件类型鉴别)
    filetype(https://github.com/h2non/filetype)是一个Go语言的第三方库,可以根据文件的魔数(magicnumbers)签名来推断文件的类型和MIME类型。它支持多种常见的文件类型,包括图片、视频、音频、文档、压缩包等。它还提供了一些便捷的函数和类型匹配器,可以方便地对文件进行分类和筛选......
  • Go每日一库之174:delve (Go 调试工具)
    简介Delve 用来调试 Go 语言开发的程序,该工具的目标是为 Go 语言提供一个简单、功能齐全的调试工具。为什么不推荐gdb• gdb对Go的调试支持是通过一个python脚本文件 src/runtime/runtime-gdb.py 扩展的,功能有限• gdb只能做到最基本的变量打印,却理解不了go......