SingleFlight:请求合并

16 篇文章 0 订阅

针对场景

SingleFlight用来合并请求,减轻对下游服务的压力

SingleFlight的作用是,多个goroutine调用同一个方法时,只让一个goroutine去调,其他goroutine等待,当调用的goroutine完成调用后,会将结果返回给其他等待的goroutine

使用SingleFlight,在面对高并发请求的场景时,而且这些请求都是读请求,可以将多个请求合并成一个请求,减轻后端服务的压力,对于像数据库这样的后端服务性能较低,使用SingleFlight就可以极大提升性能

SingleFlight常用于解决 缓存击穿的问题

实现原理

type Group struct {
	mu sync.Mutex       // 保护m的并发安全
	m  map[string]*call // 保存请求的key、请求的调用对象
}

// 请求调用对象
type call struct {
	wg sync.WaitGroup   // 用来阻塞没有执行的goroutine

	// These fields are written once before the WaitGroup is done
	// and are only read after the WaitGroup is done.
	val interface{}     // 请求执行完后的值
	err error              // 请求执行完后的error值


	forgotten bool      // 是否忘掉这个key

	dups  int      //  执行这个key的goroutine数量
	chans []chan<- Result     
}

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

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
	g.mu.Lock()
	// 初始化map
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	// key存在,说明有goroutine在处理请求,dups++后调用wg.wait阻塞等待;唤醒后,返回保存在call对象中的val、err
	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
	}

	// key不存在,创建call对象,调用wg.Add阻塞并发的goroutine,将key保存到map中证明有人在处理请求
	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
}

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
	......
	defer func() {
		// 唤醒其他等待的goroutine
		c.wg.Done()
		g.mu.Lock()
		defer g.mu.Unlock()
		// 如果forgotten为false,那么会删除请求的key,即该次请求结束后,下一次请求该key时会再执行一次fn方法
		if !c.forgotten {
			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 return
			for _, ch := range c.chans {
				ch <- Result{c.val, c.err, c.dups > 0}
			}
		}
	}()

	func() {
		defer func() {
			if !normalReturn {
				// 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 {
					c.err = newPanicError(r)
				}
			}
		}()
		// 调用fn方法,保存value、err到call对象
		c.val, c.err = fn()
		normalReturn = true
	}()
	
	......
}

实践

https://github.com/golang/groupcache
https://github.com/cockroachdb/cockroach
https://github.com/coredns/coredns

这些项目中都有用到SingleFlight

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值