singleflight中入分析

singleflight

面试被问过两次:有没有看过singleflight源码?在同一个坑里我栽了两次,下一次一定要站起来。

源码分析

Group

当我们调用 singleflight.Group{}是,返回的是一个Group类型的对象。

type Group struct {
	mu sync.Mutex       // 避免m发生并发读写问题
	m  map[string]*call // 懒加载
}

// call is an in-flight or completed singleflight.Do call
type call struct {
	wg sync.WaitGroup

    // 存储call中函数执行的结果和可能发生的错误。只在wg.Wait()返回之后才被读取。
	val interface{}
	err error

	// 记录与当前call关联的重复请求的数量
	dups  int
    
    // 只写通道的切片,用于存储那些等待当前call结果的goroutine的通道
	chans []chan<- Result
}

Do

当key已存在时,调用Do方法时会陷入阻塞,直至方法结束,返回结果。接下来看一下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)
	}
    if c, ok := g.m[key]; ok {	// key存在,等待c.wg.Wait()结束,返回val,err
		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)	// key不存在,新建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
}

key已存在时,通过c.wg.Wait()阻塞协程,直至协程恢复返回 c.val,c.err;当key不存在时,新建call,调用doCall方法执行fn

doCall

fng.doCall中执行,当执行结束时执行c.wg.Done()方法,阻塞中的goroutine恢复。

doCall方法内部定义了两个布尔类型的参数:

  • normalReturn:用于标记函数fn是否通过正常路径返回。
  • recovered:用于标记是否从panic中恢复。

执行fn,并捕获fn中可能存在的panic。如果没有发生panic,此时已经对c.val、c.err赋值。fn中可能存在runtime.Goexit(),会立即终止当前协程,并且runtime.Goexit不属于panic。

func() {
		defer func() {
			if !normalReturn {
                // 捕获panic,设置c.err
				if r := recover(); r != nil {
					c.err = newPanicError(r)
				}
			}
		}()

		c.val, c.err = fn()
		// fn 正常执行
		normalReturn = true
	}()

判断fn是否发生panic

	if !normalReturn {
		recovered = true
	}

根据normalReturn、recovered结果处理。

defer func() {
  
  // 判断是否为 rutime.Goexit
	if !normalReturn && !recovered {
		c.err = errGoexit
	}  
  
   
   g.mu.Lock()
   defer g.mu.Unlock()
   c.wg.Done()	// 此时,其他具有相同key阻塞的协程恢复
   if g.m[key] == c {	
      delete(g.m, key)
   }

   if e, ok := c.err.(*panicError); ok {
      if len(c.chans) > 0 { // 如果调用了DoChan方法,为了避免channel死锁,需要抛出panic
         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 {
      // runtime错误不需要做任何处理,已经退出了
   } else {
      // 正常返回
      for _, ch := range c.chans {
         ch <- Result{c.val, c.err, c.dups > 0}
      }
   }
}()

整体代码如下:

// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
	normalReturn := false	// 是否正常返回
	recovered := false	// 是否从panic中恢复


	defer func() {
		// 判断错误是否为runtime.Goexit
		if !normalReturn && !recovered {
			c.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 {
			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 {
			// runtime错误不需要做任何处理,已经退出了
		} else {
			// 正常返回
			for _, ch := range c.chans {
				ch <- Result{c.val, c.err, c.dups > 0}
			}
		}
	}()

	func() {
		defer func() {
			if !normalReturn {
                // 如果发生panic,捕获并处理
				if r := recover(); r != nil {
					c.err = newPanicError(r)
				}
			}
		}()
        // 执行fn函数
		c.val, c.err = fn()
        // fn正常执行
		normalReturn = true
	}()

	if !normalReturn {
		recovered = true
	}
}

DoChan

调用DoChan方法会返回一个只读的chan,后续结果会通过这个chan返回。call中会维持需要通知的chan切片,当fn执行结束后遍历返回。

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
	ch := make(chan Result, 1)	// 缓存区大小为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

删就完了。

func (g *Group) Forget(key string) {
	g.mu.Lock()
	delete(g.m, key)
	g.mu.Unlock()
}

小结

singleflight基于Group实现,Group 通过map[key]*call,快速判断key是否存在,通过sync.Metu保证map并发安全。

基于call结构体中的sync.WaitGroup实现了Do方法(相同key阻塞等待),基于 []chan <- Result 实现了 DoChan方法。

ps:感觉小结的不怎么滴,但是不小结又好像少点什么

<think>我们被要求回答关于Go语言中singleflight包的问题,包括其实现原理和使用场景。根据提供的引用,我们可以总结如下: ### 实现原理 1. **结构体设计**:singleflight的核心是`Group`结构,它管理不同键的调用。每个键对应一个`call`结构,`call`结构用于存储正在进行或已完成的函数调用结果。 2. **并发控制**:当多个并发请求请求同一个键时,第一个请求会执行实际的函数调用,后续请求则等待该调用完成,共享结果。这通过互斥锁和条件变量(或channel)实现。 3. **结果共享**:`call`结构存储了函数调用的返回值(`val`)和错误(`err`),以及一个`waitGroup`(或类似机制)用于通知等待的请求结果已就绪。 4. **避免重复调用**:通过一个映射(map)记录每个键的`call`状态,确保同一时刻只有一个调用在执行。 引用[^1]中提到,在`fn`函数执行过程中,如果发生panic,能够被捕获并传播给所有等待的请求。引用[^3]则展示了多个协程同时请求时,只有一个会执行,其余等待并共享结果。 ### 使用场景 1. **防止缓存击穿**:当缓存失效时,大量请求同时到达数据库,使用singleflight可以确保只有一个请求去加载数据,其他请求等待并共享结果。 2. **避免重复计算**:对于计算密集型操作,如果多个请求需要相同结果,使用singleflight可以避免重复计算。 ### 回答结构 我们将按照以下结构组织回答: 1. 简要介绍singleflight的作用。 2. 详细说明实现原理(结合引用中的关键点)。 3. 列举典型使用场景。 4. 提供示例代码(可选)。 5. 生成相关问题。 由于引用中提供了部分实现细节,我们将重点整合这些信息,确保回答准确。 注意:用户要求使用中文回答,数学公式使用$...$或$$...$$格式,但本问题不涉及数学公式,因此主要关注文字描述。 根据引用[^1][^2][^3],我们总结如下: - 引用[^1]说明了如何判断fn函数中是否发生panic,并解释了在同一个goroutine中执行fn时,通过检查normalReturn标志来判断是否panic。 - 引用[^2]展示了context.WithCancel的实现,但此引用与singleflight直接关联不大,可能为误用,我们主要参考[^1]和[^3]。 - 引用[^3]通过示例说明多个协程请求同一个键时,只有一个会执行,其余得到相同结果。 因此,我们主要依据[^1]和[^3]来描述实现原理。 下面开始组织回答。</think>### Go singleflight 实现原理与使用场景 #### 一、实现原理 `singleflight` 的核心思想是**对并发重复请求进行合并**,通过共享调用结果避免重复计算。其核心结构如下: ```go type Group struct { mu sync.Mutex m map[string]*call // 键值对映射 } type call struct { wg sync.WaitGroup val interface{} // 调用结果 err error // 错误信息 dup int // 重复请求计数 } ``` **工作流程**: 1. **首次请求**(以键 `key` 为例): ```mermaid graph LR A[请求key] --> B[创建call结构体] B --> C[执行实际函数fn] C --> D[保存结果val/err] D --> E[通过wg.Done通知等待者] ``` 2. **并发重复请求**: - 后续相同 `key` 的请求直接挂起在 `call.wg.Wait()` - 共享首次请求的结果 `val/err` - 通过 `dup` 计数器统计重复请求数[^3] 3. **异常处理**: - 若 `fn()` 发生 panic,会通过 `recover()` 捕获并传播给所有等待的协程 - 通过检查 `normalReturn` 标志位判断 panic 状态[^1] #### 二、使用场景 1. **防止缓存击穿** 🔑 当缓存失效时,大量并发请求穿透到数据库: ```go func GetData(ctx context.Context, key string) (string, error) { g := new(singleflight.Group) result, err, _ := g.Do(key, func() (interface{}, error) { return db.Query(key) // 仅单次执行 }) return result.(string), err } ``` 2. **计算密集型操作合并** ⚡️ 如大规模数学计算 $( \sum_{i=1}^{n} i^2 )$,避免重复计算 3. **依赖外部API的请求** 🌐 合并相同参数的API调用,减少网络开销和配额消耗 #### 三、特性分析 | 特性 | 说明 | |------|------| | 并发控制 | 通过 `sync.Mutex` + `sync.WaitGroup` 实现 | | 结果共享 | 相同 `key` 请求返回相同结果[^3] | | 资源释放 | 调用结束后立即删除 `call` 对象 | | 错误传播 | 将 panic 转换为错误传递给所有调用者[^1] | #### 四、典型问题解决方案 **场景:防止缓存雪崩** ```go func GetWithCache(key string) string { if v, ok := cache.Get(key); ok { return v.(string) } v, err, _ := flightGroup.Do(key, func() (interface{}, error) { data := fetchFromDB(key) // 关键:单次DB访问 cache.Set(key, data, ttl) return data, nil }) return v.(string) } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值