Go语言之防缓存穿透利器Singleflight

Golang源码分析之golang/sync之singleflight golang/sync库拓展了官方自带的sync库,提供了errgroup、semaphore、singleflight及syncmap四个包,本次分析singlefliht的源代码。singlefliht用于解决单机协程并发调用下的重复调用问题,常与缓存一起使用,避免缓存击穿。singleflight经常和缓存获取配合使用,可以缓解缓存击穿问题,避免同一时刻单机大量的并发调用获取数据库构建缓存。 阅读详情

来自:指月 https://www.lixueduan.com

原文:https://www.lixueduan.com/post/go/singleflight/

本文主要分析了 Golang 中的一个第三方库,防缓存击穿利器 singleflight,包括基本使用和源码分析。

1. 缓存击穿

平时开发中为了提升性能,减轻DB压力,一般会给热点数据设置缓存,如 Redis,用户请求过来后先查询 Redis,有则直接返回,没有就会去查询数据库,然后再写入缓存。

大致流程如下图所示:

singleflight

以上流程存在一个问题,cache miss 后查询DB和将数据再次写入缓存这两个步骤是需要一定时间的,这段时间内的后续请求也会出现 cache miss,然后走同样的逻辑。

这就是缓存击穿:某个热点数据缓存失效后,同一时间的大量请求直接被打到的了DB,会给DB造成极大压力,甚至直接打崩DB。

常见的解决方案是加锁,cache miss 后请求DB之前必须先获取分布式锁,取锁失败说明是有其他请求在查询DB了,当前请求只需要循环等待并查询Redis检测取锁成功的请求把数据回写到Redis没有,如果有的话当前请求就可以直接从缓存中取数据返回了。

2. singleflight

虽然加锁能解决问题,但是太重了,而且逻辑比较复杂,又是加锁又是等待的。

相比之下 singleflight 就是一个轻量级的解决方案。

Demo如下:

package main

import (
	"errors"
	"fmt"
	"log"
	"strconv"
	"sync"
	"time"

	"golang.org/x/sync/singleflight"
)

var (
	g            singleflight.Group
	ErrCacheMiss = errors.New("cache miss")
)

func main() {
	var wg sync.WaitGroup
	wg.Add(10)

	// 模拟10个并发
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			data, err := load("key")
			if err != nil {
				log.Print(err)
				return
			}
			log.Println(data)
		}()
	}
	wg.Wait()
}

// 获取数据
func load(key string) (string, error) {
	data, err := loadFromCache(key)
	if err != nil && err == ErrCacheMiss {
		// 利用 singleflight 来归并请求
		v, err, _ := g.Do(key, func() (interface{}, error) {
			data, err := loadFromDB(key)
			if err != nil {
				return nil, err
			}
			setCache(key, data)
			return data, nil
		})
		if err != nil {
			log.Println(err)
			return "", err
		}
		data = v.(string)
	}
	return data, nil
}

// getDataFromCache 模拟从cache中获取值 cache miss
func loadFromCache(key string) (string, error) {
	return "", ErrCacheMiss
}

// setCache 写入缓存
func setCache(key, data string) {}

// getDataFromDB 模拟从数据库中获取值
func loadFromDB(key string) (string, error) {
	fmt.Println("query db")
	unix := strconv.Itoa(int(time.Now().UnixNano()))
	return unix, nil
}

结果如下:

query db
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100
2021/07/17 11:04:13 1626491053454483100

可以看到 10 个请求都获取到了结果,并且只有一个请求去查询数据库,极大的减轻了DB压力。

3. 源码分析

这个库的实现很简单,除去注释大概就 100 来行代码,但是功能很强大,值得学习。

Group

type Group struct {
	mu sync.Mutex       // protects m
	m  map[string]*call // lazily initialized
}

Group 结构体由一个互斥锁和一个 map 组成,可以看到注释 map 是懒加载的,所以 Group 只要声明就可以使用,不用进行额外的初始化零值就可以直接使用。

type call struct {
	wg sync.WaitGroup
	// 函数返回值和err信息
	val interface{}
	err error

	// 是否调用了 forget 方法
	forgotten bool

    // 记录这个 key 被分享了多少次
	dups  int
	chans []chan<- Result
}

call 保存了当前调用对应的信息,map 的键就是我们调用 Do 方法传入的 key

Do

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)
	}
    // 先判断 key 是否已经存在
	if c, ok := g.m[key]; ok { // 存在则说明有其他请求在同步执行,本次请求只需要等待即可
		c.dups++
		g.mu.Unlock()
		c.wg.Wait() // / 等待最先进来的那个请求执行完成,因为需要完成后才能获取到结果,这里用 wg 来阻塞,避免了手动写一个循环等待的逻辑
        // 这里区分 panic 错误和 runtime 的错误,避免出现死锁,后面可以看到为什么这么做
		if e, ok := c.err.(*panicError); ok {
			panic(e)
		} else if c.err == errGoexit {
			runtime.Goexit()
		}
        // 最后直接从 call 对象中取出数据并返回
		return c.val, c.err, true
	}
    // 如果 key 不存在则会走到这里 new 一个 call 并执行
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c // 注意 这里在 Unlock 之前就把 call 写到 m 中了,所以 这部分逻辑只有第一次请求会执行
	g.mu.Unlock()
    
 	// 然后我们调用 doCall 去执行
	g.doCall(c, key, fn)
	return c.val, c.err, c.dups > 0
}

doCall

这个方法比较灵性,通过两个 defer 巧妙的区分了到底是发生了 panic 还是用户主动调用了 runtime.Goexit,具体信息见https://golang.org/cl/134395

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
    // 首先这两个 bool 用于标记是否正常返回或者触发了 recover
	normalReturn := false
	recovered := false

	defer func() {
		// 如果既没有正常执行完毕,又没有 recover 那就说明需要直接退出了
		if !normalReturn && !recovered {
			c.err = errGoexit
		}

		c.wg.Done() // 这里 done 之后前面的所有 wait 都会返回了
		g.mu.Lock()
		defer g.mu.Unlock()
        // forgotten 默认值就是 false,所以默认就会调用 delete 移除掉 m 中的 key
		if !c.forgotten { // 然后这里也很巧妙,前面先调用了 done,于是所有等待的请求都返回了,那么这个c也没有用了,所以直接 delete 把这个 key 删掉,让后续的请求能再次触发 doCall,而不是直接从 m 中获取结果返回。
			delete(g.m, key)
		}

		if e, ok := c.err.(*panicError); ok {
			// 如果返回的是 panic 错误,为了避免 channel 死锁,我们需要确保这个 panic 无法被恢复
			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 {
			// 如果是exitError就直接退出
		} else {
			// 这里就是正常逻辑了,往 channel 里写入数据
			for _, ch := range c.chans {
				ch <- Result{c.val, c.err, c.dups > 0}
			}
		}
	}()

	func() { // 使用匿名函数,保证下面的 defer 能在上一个defer之前执行
		defer func() {
            // 如果不是正常退出那肯定是 panic 了
			if !normalReturn {
                 // 如果 panic 了我们就 recover 掉,然后 new 一个 panic 的错误后面在上层重新 panic
				if r := recover(); r != nil {
					c.err = newPanicError(r)
				}
			}
		}()

		c.val, c.err = fn()
        // 如果我们传入的 fn 正常执行了 normalReturn 肯定会被修改为 true
        // 所以 defer 里可以通过这个标记来判定是否 panic 了
		normalReturn = true
	}()
    
    // 如果 normalReturn 为 false 就表示,我们的 fn panic 了
    // 如果执行到了这一步,也说明我们的 fn  也被 recover 住了,不是直接 runtime exit
	if !normalReturn {
		recovered = true
	}
}

逻辑还是比较复杂,我们分开来看,简化后代码如下:

func main() {
	defer func() {
		fmt.Println("defer 1")
	}()
	func() {
		defer func() {
			fmt.Println("defer 2")
		}()
		fmt.Println("fn")
	}()
	fmt.Println("根据 normalReturn 标记给 recover 赋值")
}

第一个点就是匿名函数:使用匿名函数,保证 defer 2 能在上一个 defer 1 之前执行。

因为 defer 1里面需要用到 normalReturn 标记,而这个标记又是在 defer2 中 处理的。同时为了捕获 fn 里的 panic 又必须使用 defer 来处理,所以用了一个匿名函数。

Go 中的 defer 是先进后出的,所以必须用 匿名函数保证 defer2 和 defer1 不在一个 函数里,这样 defer 2就可以先执行了。

正常执行顺序为: fn–>defer2–>根据 normalReturn 标记给 recover 赋值–>defer1

第二个就是用双重 defer 区分 panic 和 runtime.Goexit。

fn 正常执行后就会将 normalReturn 赋值为 true,然后 defer2 里根据 normalReturn 值判断 fn 是否 panic,如果 panic 了就进行 recover 捕获掉这个panic,然后把error替换为自定义的 panicError。

并且根据 normalReturn 的值来对 recovered 标记进行赋值。

最后第一个 defer 就可以根据 normalReturn + recovered 这两个标记和 err 是否为 panicError 来判断是 fn 里发生了 panic 还是说调用了 runtime.Goexit。

第三个点就是 map 的移除:

		c.wg.Done()
		g.mu.Lock()
		defer g.mu.Unlock()
		if !c.forgotten {
			delete(g.m, key)
		}

光看这里看不出具体细节,需要结合前面 Do 方法中的这段逻辑

	if c, ok := g.m[key]; ok {
		c.dups++
		g.mu.Unlock()
		c.wg.Wait() 
		return c.val, c.err, true
	}

首先doCall 中调用了c.wg.Done(),然后 Do 中的阻塞在c.wg.Wait()这里的大量请求就全部返回了,直接就 return 了。

然后 doCall 中再调用delete(g.m, key) 把 key 从 m 中移除掉。

通过这个done巧妙的让 Do 中的wait返回后直接把 key 移除掉,这样后续使用同样 key 的请求在执行c, ok := g.m[key]判断时就会重新调用 doCall 方法,再执行一次 fn 了。

如果不移除就会导致后续请求直接从 m 这里取到数据返回了,根本不会执行 fn 去db中查最新的数据,而且 m 中的数据也会越堆积越多。

DoChan

和 do 唯一的区别是 go g.doCall(c, key, fn),但对起了一个 goroutine 来执行,并通过 channel 来返回数据,这样外部可以自定义超时逻辑,防止因为 fn 的阻塞,导致大量请求都被阻塞。

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

手动移除某个 key,让后续请求能走 doCall 的逻辑,而不是直接阻塞。

func (g *Group) Forget(key string) {
	g.mu.Lock()
	if c, ok := g.m[key]; ok {
		c.forgotten = true
	}
	delete(g.m, key)
	g.mu.Unlock()
}

4. 注意事项

1. 阻塞

singleflight 内部使用 waitGroup 来让同一个 key 的除了第一个请求的后续所有请求都阻塞。直到第一个请求执行 fn 返回后,其他请求才会返回。

这意味着,如果 fn 执行需要很长时间,那么后面的所有请求都会被一直阻塞。

这时候我们可以使用 DoChan 结合 ctx + select 做超时控制

func loadChan(ctx context.Context,key string) (string, error) {
	data, err := loadFromCache(key)
	if err != nil && err == ErrCacheMiss {
		// 使用 DoChan 结合 select 做超时控制
		result := g.DoChan(key, func() (interface{}, error) {
			data, err := loadFromDB(key)
			if err != nil {
				return nil, err
			}
			setCache(key, data)
			return data, nil
		})
		select {
		case r := <-result:
			return r.Val.(string), r.Err
		case <-ctx.Done():
			return "", ctx.Err()
		}
	}
	return data, nil
}

2. 请求失败

singleflight 的实现为,如果第一个请求失败了,那么后续所有等待的请求都会返回同一个 error。

实际上可以根据下游能支撑的 rps 定时 forget 一下 key,让更多的请求能有机会走到后续逻辑。

go func() {
       time.Sleep(100 * time.Millisecond)
       g.Forget(key)
   }()

比如1秒内有100个请求过来,正常是第一个请求能执行queryDB,后续99个都会阻塞。

增加这个 Forget 之后,每 100ms 就能有一个请求执行 queryDB,相当于是多了几次尝试的机会,相对的也给DB造成了更大的压力,需要根据具体场景进去取舍。

5. 参考

https://pkg.go.dev/golang.org/x/sync/singleflight

https://golang.org/cl/134395

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#singleflight

https://lailin.xyz/post/go-training-week5-singleflight.html

深入理解 Go 语言缓解压力利器 SingleFlight 理解 SingleFlight 阅读详情

相关推荐

使用Golang的singleflight防止缓存击穿的方法

主要介绍了使用Golang的singleflight防止缓存击穿的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

Golang | 并发编程之 singleflight

singleflight 可以实现请求抑制,是预防缓存击穿的有力工具;singleflight 主要利用 mutex+waitGroup 实现请求阻塞,利用 map 缓存请求结果;注意使用 singleflight 的阻塞问题(添加超时控制)与错误问题(定期通过 Forget 刷新 key);

雨下一整晚real的博客 1307

工业视觉检测实战指南:从硬件配置到AI算法部署

本文提供了一套完整的工业视觉检测系统搭建实战指南。从硬件选型(相机、光源、镜头、工控机)到AI算法(深度学习模型)部署,详细解析了如何为生产线配置一双“会思考的眼睛”。重点探讨了深度学习算法在复杂缺陷识别中的应用,并分享了模型训练、优化及云边协同部署的实战心得,旨在帮助工程师实现从实验室Demo到7x24小时稳定运行的工业落地。

weixin_29051193的博客 460

【从零单排Golang】第十二话:用singleflight解决单服务处理高并发任务的问题

在后端开发场景,我们经常会遇到高并发的事务处理逻辑。虽然在微服务治理的机制下,能够实现多个无状态服务节点+负载均衡高并发处理请求,但对于单个服务节点来讲,如果存在某些耗时的任务需要被高并发访问,那么如果没有一套妥善的机制处理,就很容易出现大量这类任务实例堆积,导致任务返回延迟,或者节点内存暴涨的情况。今天,就来介绍Golang内部处理这种场景的一套方案:singleflight。

HiKariのTechLab 1274

TI AWR2944毫米波雷达开发板开箱:用官方Demo快速上手3D点云与热图生成

本文详细介绍了TI AWR2944毫米波雷达开发板的开箱与实战应用,包括硬件准备、软件开发环境搭建、Demo固件刷写与配置、实时可视化与数据解析等全流程指南。通过官方Demo快速上手3D点云与热图生成,帮助开发者高效掌握毫米波雷达技术在工业检测、智能交通等领域的应用。

weixin_42627853的博客 310

An AI compute of cities based on Distributed-Platform and Distributed-Databases(天池地铁流量预测)

摘要 在日益城市化的今天,城市居民出行问题逐渐成为衡量一个城市现代化能力的标准;城市出行方式多元化的同时,也对各交通系统的调度运营能力提出了更高的要求:如何提高运营效率,如何运用有限的安保力量进行有效的安检部署等问题一直在阻挠着城市交通的发展。 An AI compute of cities based on Distributed-Platform and Distributed-Databas...

Clearlove灬Star 5164

Go singlefight 源码详解|图解

singleflight 将相同的并发请求合并成一个请求,进而减少对下层服务的压力,通常用于解决缓存击穿的问题。

面向生活编程 1512

QGC 转发RTSP流 到RTMP服务器

需求: QGC安卓版在接收摄像机RTSP视频流的时候,需要将视频推送到指定的RTMP服务器。 分析: QGC在处理音视频的时候使用了GStreamer这套框架,本身这套框架是跨平台的,所以无需关系环境,只要想到相应位置,添加对应功能代码,然后配置好安卓端的依赖库即可。 添加位置: GstVideoReceiver.cpp -》start函数负责音视频的流的播放,只需在此处添加上封装好的推流函数即可。 同理在stop中添加停止推流功能。 方便起见,可在界面上配置rtmp的推流地.

qthello 2325

数据结构STL——golang实现独立请求singleFlight

github仓库存储地址:https://github.com/hlccd/goSTL 概述 ​ singleFlight,独立请求,用于做并发控制,常用于防止缓存击穿。 ​ 对于缓存来说,它一般会有一个过期时间,过期后进行删除,当在删除后的短时间内,如果突然出现了一大批对该数据的并发请求,次数他们都没有从缓存中读取到数据,然后集体涌入数据库中进行IO,引起数据库过载造成故障。 ​ 为了解决这个问题,可以通过给一组相同的请求添加一个可重入锁,即对于拥有同一个关键词的请求来说,可以视为一组相同的请求,此

l2975的博客 607

MATLAB GUI 水果成熟度分级系统设计与实现

本文基于 MATLAB GUI 设计并实现了一款水果成熟度分级系统,能够准确地评估水果的成熟程度。在实现过程中,我们运用了 MATLAB 中的函数实现对图像的处理和分析,同时使用 GUI 界面增加了系统的易用性。值得一提的是,在实际操作中,我们应该结合实际需求对系统进行相应的改进与完善,以满足不同领域和应用场景下的需求。实验结果表明,本系统可以有效地检测出水果的成熟度,并对其进行精准的评分。因此,本文设计并实现了一款基于 MATLAB GUI 的水果成熟度分级系统。三、MATLAB 代码实现。

uote_e的博客 709

手把手教你学Simulink——基于Simulink的无感FOC(滑模观测器+PLL)实战

本文详细介绍了基于Simulink的无感FOC(滑模观测器+PLL)实现方法。主要内容包括:1)SMO+PLL原理精讲,阐述滑模观测器核心思想和PLL的作用;2)系统架构全景图展示;3)Simulink建模全流程,从基础FOC框架到SMO和PLL模块的实现;4)关键参数整定指南和工程实践要点;5)仿真结果分析及常见问题解决方案。该方法可有效替代机械传感器,降低成本并提高可靠性,适用于空调压缩机、电动汽车水泵等工业场景。文章还提供了详细的调试建议和动手实践指导。

MHD0815的博客 84

singleflight---防缓存穿透利器

SingleFlight 总结 提供了三个参数Do同步通知、Dochan异步chan通知、Forget删除map的存在的键值 每次调用都会重置map 结构体是由mu sync.Mutex、m map[string]*call组成,mu用于操作的排它锁,m用于存储已有的键值的个数 如果m中存在当前键值对,就会阻塞等待,不存在的时候会加创建call结构存入m中 Dochan会异步执行,将结果放入chan通知其他阻塞的协程 阻塞的协程永的WaitGroup实现 do 的简单使用 package main i

a...Z 1666

自抗扰控制(ADRC)理论与工程实践:6.1 离散时间ADRC的推导

本文系统阐述了自抗扰控制(ADRC)的离散化方法,重点对比了前向欧拉法和基于零阶保持器(ZOH)的精确离散化两种方法。前向欧拉法计算简单但精度较低,稳定性受采样周期约束;ZOH法精度高但对大采样周期更鲁棒。文章详细推导了跟踪微分器、扩张状态观测器和控制律的离散形式,分析了采样周期选择的关键因素,为ADRC的数字实现提供了理论指导。

技术工作者 199

分布式缓存-GO(项目整体架构简介、Ubuntu 22.04 64位安装GoLang、安装Docker、解决Go module 的依赖问题)

本文介绍了分布式缓存系统GoDistributeCache的设计与实现。该项目基于groupcache架构,支持本地缓存和分布式缓存两种模式。文章首先阐述了缓存的基本概念及其应用场景,分析了在浏览器、CDN、代理服务器等多层级架构中缓存的部署位置。重点对比了本地缓存(访问速度快)与分布式缓存(数据一致性高)的特点,并提出了多级缓存方案。项目实现了缓存穿透、击穿和雪崩的防护机制,采用一致性哈希算法进行节点选择。通过该项目可以学习到Go语言并发编程、RPC通信、缓存淘汰策略等核心技术,适合作为分布式系统的入门实

Atticus的博客 3488

Unreal5.6从入门到精通之 如何实现MetaHuman数字人

本文介绍了使用HYPER3D-ChatAvatar网站生成3D头部模型,并导入Unreal Engine制作数字人的完整流程。首先上传照片生成头部模型(约2分钟),设置参数后下载模型文件。然后在UE5.6中创建项目,激活MetaHuman插件,导入头部模型并调整肤色、面部特征和身体模板。通过MetaHuman Creator组装数字人蓝图,支持添加音频/视频动画功能。关键步骤包括:取消Rigging选项节省时间、切换英文模式避免材质问题、使用Live Link Face应用实现面部捕捉等。整个流程约需5-1

TxNet 1353

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

go-zero高级并发工具集:executors上下文隔离与collection进程内缓存实战指南

在 Go 微服务开发中,**go-zero** 框架内置了两个常被低估的并发利器:**executors 批量任务执行器**和 **collection.Cache 进程内缓存**。它们分别解决"高并发下任务缓冲与执行隔离"和"热点数据缓存与击穿防护"两大痛点,让新手也能快速写出高性能、稳定的服务端代码。 ## 为什么需要并发工具集 🚀 高并发场景下,一个请求往往要扇出多个依赖调用。如果没有

gitblog_00777的博客 1016

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

SingleFlight:一个被严重低估的并发控制利器,同时让你的 LLM 调用成本控制更优雅

本文介绍了缓存击穿问题及其解决方案SingleFlight。作者首先描述了高并发场景下缓存失效导致数据库被击穿的典型问题,对比了synchronized、双检锁、分布式锁等传统方案的优缺点。重点讲解了SingleFlight的核心思想——对相同key的请求只执行一次并共享结果,而非简单互斥。文章详细展示了Java实现SingleFlight的关键代码,利用ConcurrentHashMap和CompletableFuture实现请求合并。最后通过缓存服务示例,演示了如何用SingleFlight防止缓存击穿

感谢阅读。 760

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

GO singleflight 你真的会用吗?「源码分析+详细案例」

缓存在项目中使用应该是非常频繁的,提到缓存只要了解过 singleflight ,基本都会用于缓存实现的一部分吧?

pengyachuan的博客 1395

[golang] singleflight包

golang.org/x/sync/singleflight主要是为了防止缓存被打穿后后端数据库受到不可估量的请求造成问题,这个包能够使得一个失效的key只有一次访问 原理很简单,看代码便知 // call is an in-flight or completed singleflight.Do call type call struct { wg sync.WaitGroup ...

maverickss的专栏 614

singleflight 防止缓存击穿、并发结果共享(golang官方包和go-zero实现对比)

缓存”详细大家都不陌生,在高并发系统设计中缓存是个必不可少的因素,它能够帮助按照一定规则保存处理好的解耦从而提高系统吞吐量和响应速度,我们常见的缓存组件包括Redis、ES等。但今天并不是来介绍缓存组件的,而是在实现过程中大家都会关注到的一个问题"缓存击穿",什么是缓存击穿呢?缓存击穿:平常在高并发系统中,会出现大量的请求同时查询一个key的情况,假如此时这个热key刚好失效了,就会导致大量的请求都打到数据库上面去,这种现象就是缓存击穿。

文振熙 -- 云计算技术博客站 1290

[go语言]-深入理解singleflight

文章目录@[toc]前言singleflight使用场景singleflight 简介singleflight的使用从singleflight的test探寻最简单用法验证并发重复请求singleflight的原理callGroupDo 函数参考你的鼓励也是我创作的动力 Posted by 微博@Yangsc_o 原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0 前言 最近从java转到go,来公司第一个开发工作就是对一个资源请求去

博客 2130

【Go底层】singleflight包原理

看singleflight原码之后,要实现一个请求正在执行,相同的其它请求进来时直接报错的功能也很简单,将singleflight中等待第一个请求的逻辑改为直接返回错误就可以。

qq_45795794的博客 850

singleflight 防缓存击穿 使用及原理

文章目录 以下文章来源于学点程序 ,作者silenceper singleflight 包主要是用来做并发控制,整个包的核心代码不到100行,充分利用到了map和WaitGroup的特性。 常见的场景比如防止 缓存击穿 ,我们可以来模拟一下这种场景: 缓存击穿:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到...

找不到我的路 5164

读猿码系列——6.Golang中用幂等思路解决缓存击穿的方案:singleflight

SingleFlight的作用是在处理多个goroutine同时调用同一个函数的时候,只让一个goroutine去实际调用这个函数,等到这个goroutine返回结果的时,再把结果返回给其他几个同时调用了相同函数的goroutine,这样可以减少并发调用的数量。在实际应用中也是,它能够在一个服务中减少对下游的并发重复请求。

Snippers的博客 1010

Go singleflight 源码剖析

本文深入剖析了 Go singleflight 包的源码实现和工作原理,探索单飞的奥秘。

cmy_top的博客 1133

go的singleFlight学习

并调用 runtime.Goexit() 退出。当调用 stack := debug.Stack() 时,实际上是在获取当前程序的堆栈信息,并将其存储在一个字符串类型的变量 stack 中。当调用 runtime.Goexit() 时,当前正在执行的 goroutine 会立即终止,但不会对其他 goroutine 产生影响。这个字符串包含了程序在调用 debug.Stack() 时的调用栈信息,包括函数名、文件名和行号等。通过在适当的时候调用 runtime.Goexit(),可以实现这一点。

wangkai6666的博客 806
上一篇: Kubernetes系列教程(三)---纯三层网络方案
下一篇: 一次HTTP(S)请求究竟需要多少流量?Wireshark抓包分析
探索云原生
博客等级 码龄9年 730粉丝 131原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值