go快速上手:并发编程之singleFlight

452 阅读4分钟

Go并发编程中的Singleflight模式:优雅地处理重复请求

在Go语言的并发编程实践中,处理重复请求是一个常见且需要优化的问题。当多个goroutine几乎同时请求相同的资源或执行相同的操作时,如果每次请求都独立处理,不仅会浪费计算资源,还可能因为竞态条件导致数据不一致。为了解决这个问题,Go社区提出了一种称为"Singleflight"的模式,该模式旨在确保对于相同的请求,整个程序只执行一次操作,并将结果共享给所有请求者。

什么是Singleflight?

Singleflight模式是一种并发控制模式,它通过一个中心协调点来管理对特定请求的响应。当一个新请求到达时,它会先检查是否有其他goroutine正在处理或已经处理过相同的请求。如果是,那么当前请求将等待该请求的结果,而不是重复执行相同的操作。这样,无论有多少goroutine发起相同的请求,都只会产生一次实际的操作和一次结果计算。

Go中的Singleflight实现

在Go标准库中,虽然没有直接名为"Singleflight"的包或类型,但golang.org/x/sync/singleflight包提供了一个高效的实现。这个包中的Group类型就是用来管理Singleflight请求的。

使用singleflight.Group

singleflight.Group提供了一个Do方法,该方法接受一个请求键(通常是字符串或其他可比较的类型)和一个执行函数作为参数。执行函数返回一个结果和一个可能的错误。Do方法会确保对于给定的请求键,执行函数只被调用一次。

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/singleflight"
)

func main() {
    var group singleflight.Group

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()

            // 使用context.TODO()作为示例,实际使用时可能需要传递具体的context
            result, shared, err := group.Do("unique-key", func() (interface{}, error) {
                // 模拟耗时的操作
                time.Sleep(2 * time.Second)
                return fmt.Sprintf("result-%d", i), nil
            })

            if err != nil {
                fmt.Println("Error:", err)
                return
            }

            if shared {
                fmt.Printf("Goroutine %d got shared result: %v\n", i, result)
            } else {
                fmt.Printf("Goroutine %d got unique result: %v\n", i, result)
            }
        }(i)
    }

    wg.Wait()
}

// 注意:上面的代码示例为了演示方便,执行函数内部使用了goroutine的索引i作为结果的一部分,
// 这在实际应用中可能不是预期的行为,因为多个goroutine可能会等待同一个执行结果。
// 正确的做法是让执行函数返回不依赖于外部变量的结果。

另一个例子

package main

import (
	"errors"
	"log"
	"sync"

	"golang.org/x/sync/singleflight"
)

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. 结果共享Do方法返回的第三个值是一个布尔值,表示当前请求是否共享了其他goroutine的结果。这对于调试和性能分析很有帮助。
  2. 取消请求:由于Do方法不直接支持通过context.Context来取消请求,如果你需要取消某个正在进行的请求,你可能需要设计一种机制来中断执行函数或处理其返回的结果。
  3. 错误处理:执行函数中的错误将被直接返回给调用者,因此你需要在执行函数内部妥善处理可能出现的错误。

结论

Singleflight模式是一种强大的并发控制手段,它能够在不牺牲性能的前提下,有效减少重复请求带来的资源浪费。在Go中,通过golang.org/x/sync/singleflight包,我们可以轻松地实现这一模式,并在并发编程中发挥其优势。无论是处理网络请求、缓存数据还是执行其他昂贵的操作,Singleflight都能帮助我们提升程序的效率和可靠性。 以上就是singleFlight的用法。