singlefligt使用方法和源码解读

介绍

  • sync.once保证其整个生命周期内只调用一次;而singleflight则可以保证在一定范围内其只调用一次。

背景|使用场景

  • 应对缓存击穿:加锁可以解决这个问题,但是加锁不太灵活(不能控制访问频率之类的),singlefilght可以通过定时清除的方式限制频率
  • 去除重复请求:当一定时间范围内存在了大量的重复请求,可以考虑使用:一致性hash负载均衡+singlefilght收束请求。用户根据key使用一致性hash请求到特定的服务机器上,服务对请求执行singlefilght后,再去请求下游,以此收束重复请求

用法

基础用法:合并请求,合并第一个请求执行过程中到达的所有请求。

从下面的代码和输出可以看出每次的输出query...​都是基本上每10ms一次,而其中穿插了很多次打印结果,代表着在函数真正执行过程中所有的请求都是被拦截住了,当函数执行完时,会将所有的​**结果共享给这个期间到来的所有请求**。

package mainimport ("fmt""golang.org/x/sync/singleflight""log""math/rand""time"
)func getData(id int64) string {log.Printf("query...%d\n", id)time.Sleep(10 * time.Millisecond) // 模拟一个比较耗时的操作,10msreturn "liwenzhou.com" + fmt.Sprintf("%d", id)
}func main() {log.SetFlags(log.Lmicroseconds)g := new(singleflight.Group)var i int64 = 0for {time.Sleep(time.Duration(rand.Intn(5)) * time.Millisecond)// 调用go func() {icopy := ii++v1, _, shared := g.Do("getData", func() (interface{}, error) {ret := getData(icopy)return ret, nil})log.Printf("call: v1:%v, shared:%v , i:%d\n", v1, shared, i)}()}}/**
query...1
1st call: v1:liwenzhou.com1, shared:true
2nd call: v2:liwenzhou.com1, shared:true
*/
➜  test21 git:(main)go run main.go
20:11:31.995346 query...0
20:11:32.005800 call: v1:liwenzhou.com0, shared:true , i:4
20:11:32.005799 call: v1:liwenzhou.com0, shared:true , i:4
20:11:32.005804 call: v1:liwenzhou.com0, shared:true , i:4
20:11:32.005807 call: v1:liwenzhou.com0, shared:true , i:4
20:11:32.006386 query...4
20:11:32.016671 call: v1:liwenzhou.com4, shared:true , i:9
20:11:32.016687 call: v1:liwenzhou.com4, shared:true , i:9
20:11:32.016691 call: v1:liwenzhou.com4, shared:true , i:9
20:11:32.016693 call: v1:liwenzhou.com4, shared:true , i:9
20:11:32.016694 call: v1:liwenzhou.com4, shared:true , i:9
20:11:32.017366 query...9
20:11:32.027418 call: v1:liwenzhou.com9, shared:true , i:16
20:11:32.027433 call: v1:liwenzhou.com9, shared:true , i:16
20:11:32.027436 call: v1:liwenzhou.com9, shared:true , i:16
20:11:32.027437 call: v1:liwenzhou.com9, shared:true , i:16
进阶用法1:超时控制DoChan

在基础方法中,在某一次请求执行过程中,所有到来的新的请求都会阻塞等待这个请求的执行结果。如果真正执行的过程超时出错了,其他并发的请求就只能等待。

考虑到singleflight库通常用于避免缓存击穿,需要查询外部数据库,这样的出错、超时的场景是必须要考虑的。

比如正常是10ms,但是超时时间设置的是50ms。由于并发数量并不需要真正为1,因此想12ms就停止阻塞。

// 使用DoChan进行超时控制 
func CtrTimeout(ctx context.Context, req interface{}){ch := g.DoChan(key, func() (interface{}, error) {return call(ctx, req)})select {case <-time.After(500 * time.Millisecond): returncase <-ctx.Done()returncase ret := <-ch: go handle(ret)}
}
进阶用法2:手动合并请求频率控制

在基础用法中,请求合并的频率是 一个请求的执行时间 ,希望达到自己控制合并的时间,不限制为一个请求的执行时间。

方法:另起一个协程删除对应的key​,一般是在go.Do中另起Forget​一次即可。

// 另外启用协程定时删除key,提高请求下游次数,提高成功率
func CtrRate(ctx context.Context, req interface{}){res, _, shared := g.Do(key, func() (interface{}, error) {// 另外其一个goroutine,等待一段时间后,删除key// 删除key后的调用,会重新执行Dogo func() {time.Sleep(10 * time.Millisecond)g.Forget(key)}()return call(ctx, req)})handle(res)
}

总结

singlefligt可以合并多个请求达到限频率的目的。可以使用DoChan​方法或Forget​来手动控制请求的频率和超时返回。

源码解读

singleflight的源码就一个文件,因此就放在备注里了:


// 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 //记录并发数量,用于执行后返回时候共享的结果(Shared)chans []chan<- Result //用于支持结果通过channel返回出去
}// 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的并发安全m  map[string]*call // lazily initialized //m代表任务,一个key-val代表一个任务正在执行// 任务执行完之后会从map里面被删除
}// Result holds the results of Do, so they can be passed
// on a channel.
// 保存结果的结构体,这样才能支持通过channel返回结果
type Result struct { Val    interface{}Err    errorShared bool //是否和其他协程共享的结果
}// 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] = cg.mu.Unlock()g.doCall(c, key, fn)return c.val, c.err, c.dups > 0
}// 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] = cg.mu.Unlock()go g.doCall(c, key, fn)return ch
}// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {normalReturn := falserecovered := false// use double-defer to distinguish panic from runtime.Goexit,// more details see https://golang.org/cl/134395// 这块设计非常有意思,主要是针对两种异常退出的情况使用了双重defer来保证正确处理,具体见下面注释defer func() {// the given function invoked runtime.Goexitif !normalReturn && !recovered { //结合下面代码分析,一定是出现了exitc.err = errGoexit}g.mu.Lock()defer g.mu.Unlock()c.wg.Done() //通知其他协程可以拿结果了if g.m[key] == c { //删掉任务delete(g.m, key)}if e, ok := c.err.(*panicError); ok {// In order to prevent the waiting channels from being blocked forever,// needs to ensure that this panic cannot be recovered.if len(c.chans) > 0 {go panic(e)select {} // Keep this goroutine around so that it will appear in the crash dump.} else {panic(e)}} else if c.err == errGoexit {// Already in the process of goexit, no need to call again} else {// Normal returnfor _, ch := range c.chans {ch <- Result{c.val, c.err, c.dups > 0}}}}()func() {defer func() {if !normalReturn {//到这里说明:要么发生panic,要么发生exit// Ideally, we would wait to take a stack trace until we've determined// whether this is a panic or a runtime.Goexit.//// Unfortunately, the only way we can distinguish the two is to see// whether the recover stopped the goroutine from terminating, and by// the time we know that, the part of the stack trace relevant to the// panic has been discarded.if r := recover(); r != nil { //panic就赋值c.err = newPanicError(r)}}}()c.val, c.err = fn()normalReturn = true //到这里说明:没有发生panic,任务里面没有go exit}()//执行不到这里的话说明是exitif !normalReturn { //要么发生panic,要么发生exit    recovered = true  //结合分析------> 一定是panic,所以赋值}
}// 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()
}

双重defer来确定是exit​还是发生了panic。
这块设计非常有意思,主要是针对两种异常退出的情况使用了双重defer来保证

总结

singleflight使用map​来隔离不同的任务,map​的key存在性标识任务是否在执行(执行完会马上从map中删除)。

对于一个任务,使用call结构体进行管理,其主要用于:控制并发(waitGroup​),执行,和传递结果(支持channel​传递)。

执行中很有意思的一点是使用了双重defer来判断异常退出是panic退出还是调用了exit()退出

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/85241.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/85241.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/85241.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTP 协议的基本概念(请求/响应流程、状态码、Header、方法)问题解决方案大全

HTTP 协议的基本概念&#xff08;请求/响应流程、状态码、Header、方法&#xff09;问题解决方案大全 一. 摘要 HTTP 协议是 Web 开发的基石&#xff0c;但初学者往往只停留在 GET、POST 的层面&#xff0c;对重定向机制、缓存控制、请求体解析等概念缺乏深入理解&#xff0c;…

Python中常用的函数

以下是Python中常用的函数分类整理&#xff0c;涵盖基础操作、数据处理、文件操作、面向对象等场景&#xff0c;并附上示例说明&#xff1a; --- ### **一、基础内置函数** | 函数 | 作用 | 示例 | |----…

【Windows】删除鼠标右键多余菜单的方法

要删除鼠标右键菜单中的多余菜单&#xff0c;如&#xff1a;“打开抖音壁纸”选项&#xff0c;通常需要通过修改注册表或使用第三方工具来清理残留的注册表项。以下是详细步骤&#xff08;操作注册表前务必备份&#xff01;&#xff09;&#xff1a; 方法一&#xff1a;通过注册…

【性能优化】启用zram

性能优化 系统内存不足时&#xff0c;可以考虑启动ZRAM功能&#xff08;压缩内存&#xff09;。关于ZRAM的概念&#xff0c;可自行学习。这里记录一下&#xff0c;启用ZRAM的方式。 启用ZRAM&#xff0c;可能会导致CPU升高&#xff0c;以及低内存时的恶性循环。是否启用需要综…

深度解析YOLOv8:CSPHet卷积结构如何实现极致轻量化

文章目录 一、背景介绍1.1 YOLOv8的现状1.2 降参数的必要性 二、相关技术介绍2.1 Dual思想2.2 HetConv 三、CSPHet结构设计3.1 CSP模块的改进3.2 结合HetConv3.3 参数量的下降 四、CSPHet的代码实现五、实验结果六、总结与展望 在目标检测领域&#xff0c;YOLO系列算法一直以其…

适配器模式demo

#include <QCoreApplication> #include <iostream>using namespace std;class XmCom { public:void ComByXm(){cout << "XM电源适配器只适用于小米笔记本电脑" << endl;} };class LxCom { public:virtual void ComByLx() 0;virtual ~LxCom…

数据处理考核要求-SQL测试的答案

在一个团队中&#xff0c;有业务人员。如业务人员深入理解数据处理的内容&#xff0c;会大幅度增强相互配合的效率。 针对业务人员进行针对性培训&#xff0c;还是比较容易掌握SQL的数据处理。类似与大学里面开的一门选修课。数据集选择帆软的Demo数据集。 业务人员学会SQL的…

第十七届全国大学生数学竞赛(数学类)初赛模拟试题

上周组委会发布了第十七届全国大学生数学竞赛通知&#xff0c;初赛暂定于2025年11月8日(星期六)上午9:00-11:30举行&#xff0c;同时今年新增了个亮点&#xff0c;针对与数学类的同学&#xff0c;即&#xff1a; 为提升全国大学生数学竞赛的含金量和公平性&#xff0c;并进一步…

解决: React Native iOS webview 空白页

iOS react-native-webview 之前是正常的, 升级了 react-native / react-native-webview 等 之后, 就变成了空白页. 通过下面的修改, 可以修复, 回到正常的状态. 来源: https://github.com/react-native-webview/react-native-webview/issues/3697 diff --git a/node_modules/…

VMware安装Ubuntu并实现root远程登录

前置信息 垃圾Ubuntu系统默认ssh、vim都没有&#xff01;&#xff01;&#xff01; 已踩坑cnmUbuntu处于sb安全机制要求&#xff0c;默认是禁用root直接登录的 1、修改root密码 sudo -sH &#xff08;可以让一个具有sudo权限的普通用户进入 root&#xff09; 然后就是pas…

量化面试绿皮书:20. 正态生成

文中内容仅限技术学习与代码实践参考&#xff0c;市场存在不确定性&#xff0c;技术分析需谨慎验证&#xff0c;不构成任何投资建议。 20. 正态生成 Q: 如何生成两个标准正态分布&#xff08;N(0,1)&#xff09;的随机变量&#xff0c;使它们之间的相关系数为p&#xff0c;假设…

Arduino入门教程:10、屏幕显示

飞书文档https://x509p6c8to.feishu.cn/docx/N45Pd0tA1oaC4CxUWZjc8Ekyn0b 屏幕应用场景 课程使用的SSD1306是一款128*64像素可以使用IIC驱动的OLED屏幕。 SSD1306 Oled显示模块共有4个引脚&#xff0c;标记为GND, VCC, SCL和SDA。这种Oled显示模块可以使用3.3V到5V轻松上电。…

华为云Flexus+DeepSeek征文|体验华为云ModelArts快速搭建Dify-LLM应用开发平台并创建自己dify钉钉群聊机器人

华为云FlexusDeepSeek征文&#xff5c;体验华为云ModelArts快速搭建Dify-LLM应用开发平台并创建自己dify钉钉群聊机器人 什么是华为云ModelArts 华为云ModelArts ModelArts是华为云提供的全流程AI开发平台&#xff0c;覆盖从数据准备到模型部署的全生命周期管理&#xff0c;帮…

【Pytorch】(1)Pytorch环境安装-①创建虚拟环境

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、创建Pytorch的虚拟环境 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、Anaconda环境基础操作 1.1 启动Anaconda Prompt …

如何自定义WordPress登录页面,提升用户体验和安全性

WordPress是目前最受欢迎的网站搭建平台之一&#xff0c;无论是个人博客、企业网站&#xff0c;还是电商平台&#xff0c;很多人都选择用它来搭建自己的网站。不过&#xff0c;很多WordPress用户会发现默认的登录页面相对普通&#xff0c;无法体现自己网站的特色。其实&#xf…

Coze扣子 - AI生成数字人口播视频

一、数字人介绍 数字人&#xff08;Digital Human&#xff09;是指利⽤先进的数字技术和⼈⼯智能创建的虚拟人 类形象&#xff0c;能够模拟⼈类的外貌、⾏为和情感。数字⼈不仅可以在视觉上表 现出真实的⼈类特征&#xff0c;还可以通过⾃然语⾔处理与⽤户进⾏互动。 Coze通过全…

【请关注】真实案例pg及kong安装部署

# 前提需要安装好nfs KONG_NAMESPACE="kong-api" PG_NAMESPACE="pg-ha" HARBOR_IP="harbor.rancher.com" 一、安装pg高可用####################################################################################### kubectl creat…

SSRF7 SSRF漏洞的检测方式

我们可以进入bp利用bp模块collaborator&#xff0c;进行检测&#xff1a; 我们点击复制到剪切板&#xff1a; 然后再到目标网站进行构造URL&#xff1a; http://192.168.112.12/pikachu-master/vul/ssrf/ssrf_curl.php?urlmvluewtgs390alohzqjakhu2qtwkkc81.oastify.com 然…

C++ 函数的使用

C中的函数是实现代码复用和模块化的基本单元。下面从定义、调用、参数传递、常见样式、声明和分文件编写等方面进行介绍。 1. 函数定义 函数定义包括返回类型、函数名、参数列表和函数体&#xff1a; 返回类型 函数名(参数列表) {// 函数体return 返回值; // 如果返回类型不是…

一文讲清辐射传输模型

一、为什么需要进行辐射传输反演&#xff1f; 遥感影像中&#xff0c;我们看到的是从地表和大气混合后到达传感器的总辐射信号。这个信号既包含了地物反射&#xff0c;也包含了大气分子和气溶胶的散射吸收、以及地表自身或大气的热发射。若要从中定量获得植被生理参数、水体理…