Go Singleflight介绍

736 阅读2分钟

Singleflight

使用场景

我们一般都会在对外提供服务的时候加一层缓存,来减少数据库的压力,但是会在缓存服务出现抖动或者其他情况的时候会导致出现大量的 cache miss,给数据库带来较大的压力

graph LR

R1[request 1] --> golang --cache miss n--> redis
R2[request 2] --> golang --mysql get n--> mysql
Rn[request n] --> golang

Singleflight 就是将一组相同的请求合并成一个请求,对组内所有的请求返回相同的结果,这样的话实际去请求redis或者mysql就只有一个请求了

graph LR

R1[request 1] --> golang --cache miss 1--> redis
R2[request 2] --> golang --mysql get 1--> mysql
Rn[request n] --> golang

如何用

type Group
    // Do 执行函数, 对同一个 key 多次调用的时候,在第一次调用没有执行完的时候
	// 只会执行一次 fn 其他的调用会阻塞住等待这次调用返回
	// v, err 是传入的 fn 的返回值
	// shared 表示是否真正执行了 fn 返回的结果,还是返回的共享的结果
    func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)

	// DoChan 和 Do 类似,只是 DoChan 返回一个 channel,也就是同步与异步的区别
	func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result

    // Forget 用于通知 Group 删除某个 key 这样后面继续这个 key 的调用的时候就不会在阻塞等待了
	func (g *Group) Forget(key string)

示例

package main

import (
	"errors"
	"log"
	"sync"

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

var errorNotExist = errors.New("not exist")
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中获取数据
		data, err = getDataFromDB(key)
		if err != nil {
			log.Println(err)
			return "", err
		}

		//TOOD: set cache
	} 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
}

加上singleflight,修改getData方法

//获取数据
func getData(sf *singleflight.Group,key string) (string, error) {
	data, err := getDataFromCache(key)
	if err == errorNotExist {
		//模拟从db中获取数据
		v, err, _ := sf.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
}

问题

  • 一个阻塞,全部阻塞

    context + DoChan + select

    //获取数据
    func getData(ctx context.Context,sf *singleflight.Group,key string) (string, error) {
    	data, err := getDataFromCache(key)
    	if err == errorNotExist {
    		//模拟从db中获取数据
    		resultChan := sf.DoChan(key, func() (interface{}, error) {
    			return getDataFromDB(key)
    			//set cache
    		})
    
    		select {
    		case v:= <- resultChan:
    			//TOOD: set cache
    			data = v.Val.(string)
    			return data,nil
    
    		case <-ctx.Done():
    			return "",ctx.Err()
    		}
    
    	} else if err != nil {
    		return "", err
    	}
    	return data, nil
    }
    
  • 一个出错,全部出错

    singleflight.Forget 手动释放某个 key 下次调用就不会阻塞等待了