golang- single_flight

本文解析了Go语言中singleflight包的使用,通过sync.Mutex实现并发控制,确保同一时间只有一个请求执行。讲解了Do和DoChan方法,以及如何处理异常和共享结果。

数据结构

type Group struct {
 mu sync.Mutex       // 互斥锁,保证并发安全
 m  map[string]*call // 存储相同的请求,key是相同的请求,value保存调用信息。
}
  • map是懒加载,其实就是在使用时才会初始化
  • mu是互斥锁,用来保证m的并发安全
type call struct {
 wg sync.WaitGroup
 // 存储返回值,在wg done之前只会写入一次
 val interface{}
  // 存储返回的错误信息
 err error

 // 标识别是否调用了Forgot方法
 forgotten bool

 // 统计相同请求的次数,在wg done之前写入
 dups  int
  // 使用DoChan方法使用,用channel进行通知
 chans []chan<- Result
}
// Dochan方法时使用
type Result struct {
 Val    interface{} // 存储返回值
 Err    error // 存储返回的错误信息
 Shared bool // 标示结果是否是共享结果
}

Do 方法


// 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.
// Do执行和返回给定函数的值,确保某一个时间只有一个方法被执行。如果一个重复的请求进入,则重复的请求会等待前一个执行完毕并获取相同的数据,返回值shared标识返回值v是否是传递给重复的调用的
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 {
      // 已存在则解锁,调用次数+1,
      c.dups++
      g.mu.Unlock()
       
       // 然后等待 call.wg(WaitGroup) 执行完毕,只要一执行完,所有的 wait 都会被唤醒
      c.wg.Wait()
 
       // 我的Go知识还没学到异常,暂且不表:
       // 这里区分 panic 错误和 runtime 的错误,避免出现死锁,后面可以看到为什么这么做[4] 
      if e, ok := c.err.(*panicError); ok {
         panic(e)
      } else if c.err == errGoexit {
         runtime.Goexit()
      }
      return c.val, c.err, true
   }
   // 如果我们没有找到这个 key 就 new call
   c := new(call)
    
   // 然后调用 waitgroup 这里只有第一次调用会 add 1,其他的都会调用 wait 阻塞掉
   // 所以只要这次调用返回,所有阻塞的调用都会被唤醒
   c.wg.Add(1)
   g.m[key] = c
   g.mu.Unlock()
   // 实际执行fn
   g.doCall(c, key, fn)
   return c.val, c.err, c.dups > 0
}

doCall方法

// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
    // 表示方法是否正常返回
	normalReturn := false
	recovered := false
 
	// use double-defer to distinguish panic from runtime.Goexit,
	// more details see https://golang.org/cl/134395
	defer func() {
		// the given function invoked runtime.Goexit
        // 如果既没有正常执行完毕,又没有 recover 那就说明需要直接退出了
		if !normalReturn && !recovered {
			c.err = errGoexit
		}
 
		c.wg.Done()
		g.mu.Lock()
		defer g.mu.Unlock()
         // 如果已经 forgot 过了,就不要重复删除这个 key 了
		if !c.forgotten {
			delete(g.m, key)
		}
 
        // 下面应该主要是异常处理的diamante
		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() {
        // 使用一个匿名函数来执行实际的fn
		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)
				}
			}
		}()
		
         // 方法实际执行,将值存在c.val中
		c.val, c.err = fn()
		normalReturn = true
	}()
 
	if !normalReturn {
		recovered = true
	}
}

single_flight 图解

image

例子

  • Do
package main

import (
	"errors"
	"log"
	"sync"
	"golang.org/x/sync/singleflight"
	"time"
)

var errorNotExist = errors.New("not exist")
var g singleflight.Group

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

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

//获取数据
func getData(key string) (string, error) {
	data, err := getDataFromCache(key)
	if err == errorNotExist {
		//模拟从db中获取数据
		v, err, _ := g.Do(key, func() (interface{}, error) {
			return getDataFromDB(key)
			//set cache
		})
		if err != nil {
			log.Println(err)
			return "", err
		}

		//TOOD: set cache
		data = v.(string)
	} else if err != nil {
		return "", err
	}
	return data, nil
}

//模拟从cache中获取值,cache中无该值
func getDataFromCache(key string) (string, error) {
	return "", errorNotExist
}

//模拟从数据库中获取值
func getDataFromDB(key string) (string, error) {
	log.Printf("get %s from database", key)
	return "data", nil
}

//应用场景
//1. 缓存击穿:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • DoChan
// Do chan的应用试例
func main2() {
	var singleSetCache singleflight.Group

	getAndSetCache:=func (requestID int,cacheKey string) (string, error) {
		log.Printf("request %v start to get and set cache...",requestID)
		retChan:=singleSetCache.DoChan(cacheKey, func() (ret interface{}, err error) {
			log.Printf("request %v is setting cache...",requestID)
			time.Sleep(3*time.Second)
			log.Printf("request %v set cache success!",requestID)
			return "VALUE",nil
		})

		var ret singleflight.Result

		timeout := time.After(5 * time.Second)

		select {//加入了超时机制
		case <-timeout:
			log.Printf("time out!")
			return "",errors.New("time out")
		case ret =<- retChan://chan中取出结果
			return ret.Val.(string),ret.Err
		}
		return "",nil
	}

	cacheKey:="cacheKey"
	for i:=1;i<10;i++{
		go func(requestID int) {
			value,_:=getAndSetCache(requestID,cacheKey)
			log.Printf("request %v get value: %v",requestID,value)
		}(i)
	}
	time.Sleep(20*time.Second)
}

参考文档

https://blog.youkuaiyun.com/dingyu002/article/details/117958061

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值