关于golang:Golang中的并发原语-Singleflight

8次阅读

共计 4178 个字符,预计需要花费 11 分钟才能阅读完成。

在 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 main

import (
    "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 begin
2020/11/20 12:11:46 db query end
2020/11/20 12:11:46 request 6 end!
2020/11/20 12:11:46 request 6 get value: orderIDabcxyz
2020/11/20 12:11:46 request 8 get value: orderIDabcxyz
2020/11/20 12:11:46 request 2 get value: orderIDabcxyz
2020/11/20 12:11:46 request 9 get value: orderIDabcxyz
2020/11/20 12:11:46 request 7 get value: orderIDabcxyz
2020/11/20 12:11:46 request 3 get value: orderIDabcxyz
2020/11/20 12:11:46 request 4 get value: orderIDabcxyz
2020/11/20 12:11:46 request 1 get value: orderIDabcxyz
2020/11/20 12:11:46 request 5 get value: orderIDabcxyz
main end

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

总结

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

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

正文完
 0