在Golang中有一个并发原语是Singleflight,如同晓得的开发者并不多。其中驰名的 https://github.com/golang/groupcache 就用到了这个并发原语。

Golang版本

go1.15.5

相干知识点

map、Mutex、channel、

应用场景

个别用在对指定资源频繁操作的状况下,如高并发下的“缓存击穿”问题。

缓存击穿:一个存在的key,在缓存过期的霎时,同时有大量的申请过去,造成所有申请都去DB读取数据,这些申请都会击穿缓存到DB,造成刹时DB申请量大、压力霎时骤增,导致数据库负载过高,影响整个零碎失常运行。(缓存击穿不同于 缓存雪崩 和 缓存穿透)

缓存击穿

怎么了解这个原语呢,简略的讲就是将对同一个资源的多个申请合并为一个申请。

举例说明,如果当有10万个申请来获取同一个key的值的时候,失常状况下会执行10万次get操作。而应用singleflight并发语后,只须要首次的地个申请执行一次get操作就能够了,其它申请再过去时,只须要只须要期待即可。待执行后果返回后,再把后果别离返回给期待中的申请,每个申请再返回给客户端,由此看看,在肯定的高并发场景下能够大大减少零碎的负载,节俭大量的资源。

留神这个与 sync.Once是不一样的,sync.Once 是全局只能有一个,但本并发原语则是依据key来划分的,并且能够依据需要来决定什么状况下共用一个。

实现原理

次要应用 Mutext 和 Map 来实现,以 key 为键,值为 call。每个call中存储有一个申请 chans 字段,用来存储所有申请此key的客户端,等有返回后果的时候,再从chans字段中读取进去,别离写入即可。

源文件为 /src/internal/singleflight/singleflight.go

Singleflight 数据结构如下

  1. Do()
    这个办法是一个执行函数并返回执行后果
    参数
    key 要申请的key,多个申请可能申请的是同一个key,同时也只有一个函数在执行
    fn 对应执行函数,此函数有三个返回值v, err, shared。其中shared示意以后返回后果是否为多个申请后果
  2. DoChan()
    类型与Do()办法,但返回的是个 ch 类型,等函数 fn 执行后,能够通过读取返回的ch来获取函数后果
  3. Forget()
    在官网库internal/singleflight/singleflight.go中这个名字是ForgetUnshared, 通知 Group 遗记申请的这个key,下次再申请时,间接当作新的key来解决就能够了。其实就是将这个key从map中删除,后续再有这个key的操作的话,即视为新一轮的解决逻辑。

其中Do()DoChan() 的性能一样,只是获取数据的形式有所区别,开发者能够依据本人的状况来抉择应用哪一个。另外还蕴含一个由Do()办法调用的公有办法 doCall(),间接执行key解决办法的函数

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {...}

实现数据结构

实现外部次要的数据结构一共三个,别离是 call、Group 和 Result。

// 定义 map[key] 的值 call 类型// 在call 外部存储对应key相干的所有申请客户端信息type call struct {    wg sync.WaitGroup    // These fields are written once before the WaitGroup is done    // and are only read after the WaitGroup is done.        // 申请返回后果,会在sync.WaitGroup为Done的时候执行    val interface{}    err error    // These fields are read and written with the singleflight    // mutex held before the WaitGroup is done, and are read but    // not written after the WaitGroup is done.        // 能够了解为申请key的个数,每减少一个申请,则值加1     dups  int        // 存储所有key对应的 Result{}, 一个申请对应一个 Result    chans []chan<- Result}// Group represents a class of work and forms a namespace in// which units of work can be executed with duplicate suppression.// SingleFlight 的主构造体type Group struct {    mu sync.Mutex       // protects m    m  map[string]*call // lazily initialized}// Result holds the results of Do, so they can be passed// on a channel.// 定义申请后果数据结构type Result struct {    Val    interface{}    Err    error    Shared bool}

call 数据结构是用来记录key相干数据,即申请以后key的个数和申请后果
Group 是并发原语 SingleFlight 的主数据结构, m 字段用来存储key与申请的关系,而mu则是Mutex锁
Result 申请后果,其中 Val 是实现申请后返回的后果,Err 示意是否出错,而 Shared 字段则示意是否为多个申请后果。如果以后key只有一个申请的话,则返回false, 否则返回 true

用法

package mainimport (    "fmt"    "golang.org/x/sync/singleflight"    "log"    "sync"    "time")func getDataFromDB(key string) (string, error) {    defer func() {        log.Println("db query end")    }()    log.Println("db query begin")    time.Sleep(2 * time.Second)    result := key + "abcxyz"    return result, nil}func main() {    var singleRequest singleflight.Group    getData := func(requestID int, key string) (string, error) {        log.Printf("request %v start request ...", requestID)        // 合并申请        value, _, _ := singleRequest.Do(key, func() (ret interface{}, err error) {            log.Printf("request %v is begin...", requestID)            ret, err = getDataFromDB(key)            log.Printf("request %v end!", requestID)            return        })        return value.(string), nil    }    var wg sync.WaitGroup    key := "orderID"    for i := 1; i < 10; i++ {        wg.Add(1)        go func(wg *sync.WaitGroup, requestID int) {            defer wg.Done()            value, _ := getData(requestID, key)            log.Printf("request %v get value: %v", requestID, value)        }(&wg, i)    }    wg.Wait()    fmt.Println("main end")}

输入后果

2020/11/20 12:11:44 request 6 start request …2020/11/20 12:11:44 request 6 is begin…2020/11/20 12:11:44 request 2 start request …2020/11/20 12:11:44 request 9 start request …2020/11/20 12:11:44 request 7 start request …2020/11/20 12:11:44 request 3 start request …2020/11/20 12:11:44 request 4 start request …2020/11/20 12:11:44 request 1 start request …2020/11/20 12:11:44 request 5 start request …2020/11/20 12:11:44 request 8 start request …2020/11/20 12:11:44 db query begin2020/11/20 12:11:46 db query end2020/11/20 12:11:46 request 6 end!2020/11/20 12:11:46 request 6 get value: orderIDabcxyz2020/11/20 12:11:46 request 8 get value: orderIDabcxyz2020/11/20 12:11:46 request 2 get value: orderIDabcxyz2020/11/20 12:11:46 request 9 get value: orderIDabcxyz2020/11/20 12:11:46 request 7 get value: orderIDabcxyz2020/11/20 12:11:46 request 3 get value: orderIDabcxyz2020/11/20 12:11:46 request 4 get value: orderIDabcxyz2020/11/20 12:11:46 request 1 get value: orderIDabcxyz2020/11/20 12:11:46 request 5 get value: orderIDabcxyzmain end

从下面的输入内容能够看到,咱们同时发动了10个gorourtine来查问同一个订单信息,真正在DB 层查问操作只有一次,大大减少了DB的压力。

总结

应用SingleFlight时,在高并发下且对大量key频繁读取的话,能够大大减少服务器负载。在肯定场景下特地的有用,如 CDN。

举荐 Golang并发原语之-信号量Semaphore