Go语言设计与实现 -- singleflight

news2024/9/26 5:17:13

这个东西很重要,可以经常用在项目当中,所以我们单独拿出来进行讲解。

在使用它之前我们需要导包:

 go get golang.org/x/sync/singleflight

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

golang-query-without-single-flight

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

golang-extension-single-flight

使用方法

singleflight类的使用方法就新建一个singleflight.Group,使用其方法Do或者DoChan来包装方法,被包装的方法在对于同一个key,只会有一个协程执行,其他协程等待那个协程执行结束后,拿到同样的结果

  • Group结构体

    代表一类工作,同一个group中,同样的key同时只能被执行一次

  • Do方法

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

    key:同一个key,同时只有一个协程执行

    fn:被包装的函数

    v:返回值,即执行结果。其他等待的协程都会拿到

    shared:表示是否由其他协程得到了这个结果v

  • DoChan方法

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

    Do差不多其实,因此我们就只讲解Do的实际应用场景了。

具体应用场景

var singleSetCache singleflight.Group

func GetAndSetCache(r *http.Request, cacheKey string) (string, error) {
	log.Printf("request %s start to get and set cache...", r.URL)
	value, err, _ := singleSetCache.Do(cacheKey, func() (interface{}, error) {
		log.Printf("request %s is getting cache...", r.URL)
		time.Sleep(3 * time.Second)
		log.Printf("request %s get cache success!", r.URL)
		return cacheKey, nil
	})
	return value.(string), err
}

func main() {
	r := gin.Default()
	r.GET("/sekill/:id", func(context *gin.Context) {
		ID := context.Param("id")
		cache, err := GetAndSetCache(context.Request, ID)
		if err != nil {
			log.Println(err)
		}
		log.Printf("request %s get value: %v", context.Request.URL, cache)
	})
	r.Run()
}

来看一下执行结果:

2022/12/29 16:21:18 request /sekill/5 start to get and set cache...
2022/12/29 16:21:18 request /sekill/5 is getting cache...
2022/12/29 16:21:18 request /sekill/9 start to get and set cache...
2022/12/29 16:21:18 request /sekill/9 is getting cache...
2022/12/29 16:21:18 request /sekill/9 start to get and set cache...
2022/12/29 16:21:18 request /sekill/5 start to get and set cache...
2022/12/29 16:21:18 request /sekill/5 start to get and set cache...
2022/12/29 16:21:18 request /sekill/9 start to get and set cache...
2022/12/29 16:21:18 request /sekill/9 start to get and set cache...
2022/12/29 16:21:18 request /sekill/5 start to get and set cache...
2022/12/29 16:21:19 request /sekill/9 start to get and set cache...
2022/12/29 16:21:19 request /sekill/5 start to get and set cache...
2022/12/29 16:21:21 request /sekill/9 get cache success!
2022/12/29 16:21:21 request /sekill/5 get cache success!
2022/12/29 16:21:21 request /sekill/5 get value: 5
2022/12/29 16:21:21 request /sekill/5 get value: 5
[GIN] 2022/12/29 - 16:21:21 | 200 |    3.0106529s |       127.0.0.1 | GET      "/sekill/5"
2022/12/29 16:21:21 request /sekill/9 get value: 9
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.8090881s |       127.0.0.1 | GET      "/sekill/5"
2022/12/29 16:21:21 request /sekill/9 get value: 9
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.2166003s |       127.0.0.1 | GET      "/sekill/9"
2022/12/29 16:21:21 request /sekill/9 get value: 9
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.6064069s |       127.0.0.1 | GET      "/sekill/9"
2022/12/29 16:21:21 request /sekill/9 get value: 9
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.4178652s |       127.0.0.1 | GET      "/sekill/9"
2022/12/29 16:21:21 request /sekill/9 get value: 9
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.8101267s |       127.0.0.1 | GET      "/sekill/9"
2022/12/29 16:21:21 request /sekill/5 get value: 5
[GIN] 2022/12/29 - 16:21:21 | 200 |    3.0116892s |       127.0.0.1 | GET      "/sekill/9"
2022/12/29 16:21:21 request /sekill/5 get value: 5
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.6074537s |       127.0.0.1 | GET      "/sekill/5"
2022/12/29 16:21:21 request /sekill/5 get value: 5
[GIN] 2022/12/29 - 16:21:21 | 200 |    2.4076473s |       127.0.0.1 | GET      "/sekill/5"
[GIN] 2022/12/29 - 16:21:21 | 200 |     2.218686s |       127.0.0.1 | GET      "/sekill/5"

可以看到确实只有一个协程执行了被包装的函数,并且其他协程都拿到了结果。

接下来我们来看一下它的原理吧!

原理

首先来看一下Group结构体:

type Group struct {
   mu sync.Mutex  // 锁保证并发安全   
   m  map[string]*call //保存key对应的函数执行过程和结果的变量。
}

然后我们来看一下call结构体:

type call struct {
    wg sync.WaitGroup //用WaitGroup实现只有一个协程执行函数
    val interface{} //函数执行结果
    err error
    forgotten bool
    dups  int  //含义是duplications,即同时执行同一个key的协程数量
    chans []chan<- Result

}

然后我们来看一下Do方法:

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
    // 写Group的m字段时,加锁保证写安全
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
        // 如果key已经存在,说明已经由协程在执行,则dups++并等待其执行结果,执行结果保存在对应的call的val字段里
		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
	}
    // 如果key不存在,则新建一个call,并使用WaitGroup来阻塞其他协程,同时在m字段里写入key和对应的call
	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
}

然后我们来分析一下doCall函数:

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()
}
  1. 运行传入的函数 fn,该函数的返回值会赋值给 c.valc.err
  2. 调用 sync.WaitGroup.Done 方法通知所有等待结果的 Goroutine — 当前函数已经执行完成,可以从 call 结构体中取出返回值并返回了;
  3. 获取持有的互斥锁并通过管道将信息同步给使用 golang/sync/singleflight.Group.DoChan 方法的 Goroutine

问题分析

分析了源码之后,我们得出了一个结论,这个东西是用阻塞来实现的,这就引发了一个问题:如果我们处理的那个请求刚好遇到问题了,那么后面的所有请求都会被阻塞,也就是,我们应该加上适合的超时控制,如果在一定时间内,没有获得结果,那么就当作超时处理。

于是这个适合我们应该使用DoChan()。两者实现上完全一样,不同的是, DoChan() 通过 channel 返回结果。因此可以使用 select 语句实现超时控制。

var singleSetCache singleflight.Group

func GetAndSetCache(r *http.Request, cacheKey string) (string, error) {
   log.Printf("request %s start to get and set cache...", r.URL)
   retChan := singleSetCache.DoChan(cacheKey, func() (interface{}, error) {
      log.Printf("request %s is getting cache...", r.URL)
      time.Sleep(3 * time.Second)
      log.Printf("request %s get cache success!", r.URL)
      return cacheKey, nil
   })
   var ret singleflight.Result
   timeout := time.After(2 * time.Second)
   select {
   case <-timeout:
      log.Println("time out!")
      return "", errors.New("time out")
   case ret = <-retChan: // 从chan中获取结果
      return ret.Val.(string), ret.Err
   }
}

func main() {
   r := gin.Default()
   r.GET("/sekill/:id", func(context *gin.Context) {
      ID := context.Param("id")
      cache, err := GetAndSetCache(context.Request, ID)
      if err != nil {
         log.Println(err)
      }
      log.Printf("request %s get value: %v", context.Request.URL, cache)
   })
   r.Run()
}

补充

这里其实还有一个Forget方法,它可以在映射表中删除某个键,接下来对键的调用就不会等待前面的函数返回了。

总结

当然,如果单次的失败无法容忍,在高并发的场景下更好的处理方案是:

  1. 放弃使用同步请求,牺牲数据更新的实时性
  2. “缓存” 存储准实时的数据 + “异步更新” 数据到缓存

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【NCC】之二:积分图加速均值计算

文章目录<center> 积分图 integral image1. 原理&#xff1a;2. 示例3. 计算区域均值4. 计算区域方差5. 积分图示例6. 计算积分图的源码7. 用积分图加速NCC参考积分图 integral image1. 原理&#xff1a; Summed Area Table是一种数据结构和算法&#xff0c;用于快速有效…

【math】大规模对称正定稀疏线性方程组的求解与代数多重网格

大规模对称正定稀疏线性方程组的求解与代数多重网格代数多重网格问题定义迭代法的优畧几何多重网格代数多重网格代数多重网格 你好&#xff01;代数多重网格一个很有意思的话题。 问题定义 很多问题都可以抽象为求解下列优化的问题&#xff1a; 对于图像问题&#xff0c;一…

安全、稳定的工业蜂窝路由器具有怎样的特性?

一、前言 传统路由器通过电缆或光纤线路访问Internet&#xff0c;在很多场景或区域下存在着很大的局限性&#xff0c;例如在行驶的火车上&#xff0c;在固定电话稀缺或没有其他接入方式的地区都是十分受限的。随着科技的发展&#xff0c;很多行业应用都需要具有更强大功能的路…

3 高级面向对象编程实例

高级OOP 1 继承 是一种基于已有类创建新类的机制 class 子类名 extends 父类{类体; }public class Extends_v1 {public static void main(String[] args) {Extendsclass01 ex new Extendsclass01();} } class Baseclass01{public int num;public void setNum(int n){num n…

java之线程死锁和ThreadLocal的使用

线程死锁&#xff1a; 线程死锁是指两个或者两个以上的线程在执行过程中&#xff0c;由于竞争资源或者彼此通信而造成的一种阻塞的现象,若无外力的作用,它们都将无法继续执行下去。 此时应用系统就处于了死锁状态&#xff0c;这些永远在互相等待的线程称为死锁线程。 如下图…

文本中按规则分组区段随机抽样

【问题】 This is a bit complex, and I greatly appreciate any help! I am trying to randomly sample rows from a .csv file. Essentially, I want a resulting file of unique locations (Locations are specified by Easting and Northing columns of the data file, be…

ServletContext和过滤器

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;JAVA开发者…

BM30 二叉搜索树与双向链表

题目 输入一棵二叉搜索树&#xff0c;将该二叉搜索树转换成一个排序的双向链表。如下图所示&#xff1a; 数据范围&#xff1a;输入二叉树的节点数0≤n≤1000&#xff0c;二叉树中每个节点的值0≤val≤1000. 要求&#xff1a;空间复杂度O(1)&#xff08;即在原树上操作&#x…

低代码对比分析,从工程化上看产品的优劣

低代码算是这几年在IT行业内越来越尖锐的讨论了&#xff0c;而且随着这两年大厂的大量裁员&#xff0c;更是亲者痛仇者快的事情&#xff0c;因为很多大厂发现把一些低端的研发岗位干掉了&#xff0c;反而整个体系在工具的辅助运转下&#xff0c;效率更高&#xff0c;执行力更优…

【Python数据分析】Python模拟登录(一) requests.Session应用

最近由于某些原因&#xff0c;需要用到Python模拟登录网站&#xff0c;但是以前对这块并不了解&#xff0c;而且目标网站的登录方法较为复杂&#xff0c; 所以一下卡在这里了&#xff0c;于是我决定从简单的模拟开始&#xff0c;逐渐深入地研究下这块。 注&#xff1a;本文仅为…

Python学习基础笔记五十九——封装和@property

1、私有属性的一个用法&#xff1a; class Room:def __init__(self, name, length, width):self.name nameself.__length lengthself.__width widthdef area(self):return self.__length * self.__widthwei Room(Wei, 2, 1) print(wei.area()) 2、getter和setter&#xf…

Hi3861鸿蒙物联网项目实战:智能照明灯

华清远见FS-Hi3861开发套件&#xff0c;支持HarmonyOS 3.0系统。开发板主控Hi3861芯片内置WiFi功能&#xff0c;开发板板载资源丰富&#xff0c;包括传感器、执行器、NFC、显示屏等&#xff0c;同时还配套丰富的拓展模块。开发板配套丰富的学习资料&#xff0c;包括全套开发教程…

第十篇 1+X考证 Web前端测试题(新)

单选题 1、关于HTML和CSS以下说法错误的是&#xff08; D &#xff09; A、HTML标签中属性的值一定要用双引号或单引号括起来B、HTML空元素要有结束的标签或于开始的标签后加上"/"C、结构与样式完全分离时&#xff0c;结构代码中不涉及任何的样式元素&#xff0c;如f…

Qt之软键盘的实现

文章目录前言一、基于中文汉字数据库1、核心代码2、效果二、基于谷歌拼音输入引擎1、核心代码2、效果前言 Qt5.8版本开始推出了基于QML实现的软键盘功能&#xff0c;在此之前&#xff0c;并没有官方版本的软键盘。本篇主要介绍Qt实现软键盘的两种方案&#xff0c;一种基于中文汉…

[python][GUI]pyside6

------------------------------------------------------------------------------------------------------------------ #非常好资料和教程&#xff1a; 1. Module Index - Qt for Python 2. muziing/PySide6-Code-Tutorial: 可能是最好的PySide6中文教程&#xff01;用代…

Spring boot 日志直接推送到elasticsearch上

Spring boot 日志直接推送到elasticsearch前言核心依赖elasticsearch配置文件1.url格式如下2.index索引前缀 "xxx"3.maxMessageSize参数数据内容最大值&#xff0c;本文未使用&#xff08;默认值-1全部数据接收&#xff09;如下4.BasicAuthentication.java 重写该类用…

详解opencv库函数ellipse()

opencv库函数ellipse()函数可以画扇形&#xff0c;也可以画椭圆。画扇形时只需要将椭圆的长短轴长度设为相同并给定扇形的圆心角即可。 # 参数 1.目标图片 2.椭圆圆心 3.长短轴长度 4.偏转角度 5.圆弧起始角度 6.终止角度 7.颜色 8.是否填充 cv2.ellipse(img_p, (500, 2…

[python] PyMouse、PyKeyboard用python操作鼠标和键盘

1、PyUserInput 简介 PyUserInput是一个使用python的跨平台的操作鼠标和键盘的模块&#xff0c;非常方便使用。支持的平台及依赖如下&#xff1a; Linux - XlibMac - Quartz, AppKitWindows - pywin32, pyHook 支持python版本&#xff1a;我用的是3.6.7 2、安装 直接源码安装…

越南猫年来袭!2023Lazada年货节热销品趋势

距离2023年春节倒计时23天&#xff01;大家是否对春节假期已经满怀期待了&#xff1f;越南人也和我们一样正期盼着新年到来&#xff0c;越南所有的传统节日都是按照农历来算的&#xff0c;其中春节也是越南重大的节日。春节将至&#xff0c;提前置办年货成了越南人和华人必不可…

MySQL 表的增删改查(进阶篇②)· 联合查询 内连接 外连接 · 自连接 · 子查询 exists · 合并查询 union

接进阶篇①&#xff0c;我们继续学习。 一、联合查询1.1 内连接1.2 外连接1.3 内连接和左右外连接的区别二、自连接三、子查询3.1 单行子查询3.2 多行子查询使用 in 范围匹配多行另一种写法 exists两种写法的区别3.3 在 from 子句中使用子查询四、合并查询unionunion all一、联…