What’s SingleFlight

Hello everyone, today I would like to introduce to you the Go Singleflight package, which is not a direct translation of the meaning of solo flight. SingleFlight is another concurrency primitive provided by the Go language Sync extension library. What problem is SingleFlight designed to solve? The official document explains:

Package singleflight provides a duplicate function call suppression mechanism.

The SingleFlight package provides a mechanism to suppress repeated function calls.

In terms of the Go program, SingleFlight allows only one Goroutine to call the function when multiple Goroutines call the same function at the same time. When the goroutine returns the result, The result is then returned to several other Goroutines that simultaneously called the same function, reducing the number of concurrent calls. In practice, too, it can reduce the number of concurrent repeated requests to downstream within a service. Another common use scenario is to prevent cache breakdowns.

SingleFlight by Go

The Go extension provides the singleFlight concurrency primitive with the SingleFlight.group structure type.

The singleflight.Group type provides three methods:

func (g *Group) Do(key string, fn func(a) (interface{}, error)) (v interface{}, err error, shared bool)

func (g *Group) DoChan(key string, fn func(a) (interface{}, error)) < -chan Result

func (g *Group) Forget(key string)
Copy the code
  • DoMethod, which takes a string Key and a function to be called, returns the result of calling the function and errors. When the Do method is used, it determines whether to actually call it based on the Key providedfnFunction. The same key is executed only the first time the Do method is called at the same timefnFunction, and other concurrent requests wait for the result of the call.
  • DoChanMethod: Similar to the Do method, but an asynchronous call. It will return a channel, etcfnAfter the function executes and produces the result, it can receive the result from the chan.
  • ForgetMethod: Delete a Key in SingleFlight. That way, a subsequent call to the Do method on the Key will be executedfnFunction instead of waiting for the previous unfinishedfnThe result of the function.

Application scenarios

Now that you know what methods can be called by the SingleFlight concurrency primitive provided by the Go language, here are two application scenarios for it.

Querying DNS Records

The lookupGroup structure used in the Go LANGUAGE NET standard library is singleflight.group, a primitive provided by the Go extension library

type Resolver struct{.../ / source address https://github.com/golang/go/blob/master/src/net/lookup.go#L151
	// lookupGroup merges LookupIPAddr calls together for lookups for the same
	// host. The lookupGroup key is the LookupIPAddr.host argument.
	// The return values are ([]IPAddr, error).
	lookupGroup singleflight.Group
}
Copy the code

Is the role of it for the same domain DNS record query merged into a query, the following is the.net libraries provide DNS record query methods using LookupIp lookupGroup this SingleFlight merged queries related to the source code, it USES asynchronous DoChan query method.

func LookupIP(host string) ([]IP, error) { addrs, err := DefaultResolver.LookupIPAddr(context.Background(), host) ...... } func (r *Resolver) lookupIPAddr(ctx context.Context, network, host string) ([]IPAddr, error) { ...... Ch, called := r.getlookupgroup ().dochan (lookupKey, func() (interface{}, error) { defer dnsWaitGroup.Done() return testHookLookupIP(lookupGroupCtx, resolverFunc, network, host) }) if ! called { dnsWaitGroup.Done() } select { case <-ctx.Done(): ...... case r := <-ch: lookupGroupCancel() if trace ! = nil && trace.DNSDone ! = nil { addrs, _ := r.Val.([]IPAddr) trace.DNSDone(ipAddrsEface(addrs), r.Shared, r.Err) } return lookupIPReturn(r.Val, r.Err, r.Shared) } }Copy the code

The source code above has been deleted a lot. Only the SingleFlight merge query section is left. If you are interested, you can go to GitHub to see the full source code at github.com/golang/go/b… , can be directly located to this part of the source.

Network management is not very intimate, remember three even ah ~!

Preventing cache breakdown

When using caching in a project, a common use is to query data in the cache, and if not, to find the data in the database and cache it in Redis. Cache breakdown refers to the problem that in a high-concurrency system, when a large number of requests query a cache Key at the same time, if the Key expires, a large number of requests will be sent to the database, which is called cache breakdown. SingleFlight is a good way to solve the cache breakdown problem, where only one of these concurrent requests to the same Key can be queried in the database, and these concurrent requests can share the same result.

The following is a simulation of using the SingleFlight concurrency primitive to merge the Redis cache. You can test it yourself by opening 10 Goroutines to query a fixed Key and observing the result. You will find that only one Redis query is executed.

Struct {//... RequestGroup singleflight.Group} func (c *client) Get(key string) (interface{}, error) { fmt.Println("Querying Database") time.Sleep(time.Second) v := "Content of key" + key return v, SingleFlight query func (c *client) SingleFlightGet(key String) (interface{}, error) {v, err, _ := c.requestGroup.Do(key, func() (interface{}, error) { return c.Get(key) }) if err ! = nil { return nil, err } return v, err }Copy the code

The full test source can be downloaded from my GitHub repository

Realize the principle of

Finally, let’s take a look at the implementation principle of Singleflight. Group, through its source code is also able to learn a lot of Go language programming skills. Singleflight. Group consists of a Mutex sync.Mutex and a mapping table. Each singleFlight. call structure holds information about the current call:

type Group struct {
	mu sync.Mutex
	m  map[string]*call
}

type call struct {
	wg sync.WaitGroup

	val interface{}
	err error

	dups  int
	chans []chan<- Result
}
Copy the code

Let’s look at how the Do and DoChan methods are implemented.

Do method

SingleFlight defines a call structure, each of which holds the information corresponding to the FN call.

The execution logic of the Do method is that each time the Do method is called, the mutex will be obtained first, and then the call structure of the FN function corresponding to the Key will be determined in the mapping table.

  • When none exists, the proof is the first request for this Key, and one is initializedcallStructure pointer, increaseSingleFlightInternally heldsync.WaitGroupThe counter goes to 1. Release the mutex and block for the doCall method to executefnThe return result of the function
  • Increases when presentcallIn vivo representation of structurefnA counter for the number of repeated callsdups, release the mutex, and then useWaitGroupWaiting for thefnThe function completes execution.

The val and err fields of the call structure are assigned only after fn returns a result from the doCall method, so when doCall and waitgroup. Wait return, the results and errors of the function call are returned to all callers of the Do method.


  func (g *Group) Do(key string, fn func(a) (interface{}, error)) (v interface{}, err error, shared bool) {
    g.mu.Lock()
    if g.m == nil {
      g.m = make(map[string]*call)
    }
    if c, ok := g.m[key]; ok {
      // If the same key exists, increase the count
      c.dups++
      g.mu.Unlock()
      c.wg.Wait() // Wait for the fn call for this key to complete
      return c.val, c.err, true // Returns the result of the fn call
    }
    c := new(call) // There is no key, it is the first request, create a call structure
    c.wg.Add(1)
    g.m[key] = c // Add to the mapping table
    g.mu.Unlock()
  

    g.doCall(c, key, fn) // Call the method
    return c.val, c.err, c.dups > 0
  }
Copy the code

The doCall method will actually call the fn function, because the default value of the forgotten field after the call structure is initialized is false, and the corresponding Key will be deleted after the return of the FN call. The fn function will be called again for the next round of requests using the same Key.

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { c.val, c.err = fn() c.wg.Done() g.mu.Lock() if ! C. forgotten {// the call has been completed, Delete (g.m, key)} for _, ch := range c.c Hans {ch < -result {c.val, c.edr, c.ups > 0}} g.m.Copy the code

DoChan method

SingleFlight also provides asynchronous calls to the DoChan method. Its execution logic is similar to that of the Do method, except that instead of blocking and waiting for the call to return, the DoChan method creates a Chan Result channel to return to the caller, through which the caller can receive the results of the FN function. The Chan Result channel is placed in the call structure’s maintenance notification queue before being returned to the caller. When the fn function returns the Result, the DoChan method sends the Result to each channel in the notification queue.

func (g *Group) DoChan(key string, fn func(a) (interface{}, error)) < -chan Result {
	ch := make(chan Result, 1)
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call)
	}
	if c, ok := g.m[key]; ok {
		c.dups++
		c.chans = append(c.chans, ch)
		g.mu.Unlock()
		return ch
	}
	c := &call{chans: []chan<- Result{ch}}
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	go g.doCall(c, key, fn)

	return ch
}

type Result struct {
	Val    interface{}
	Err    error
	Shared bool
}
Copy the code

conclusion

Once you have learned SingleFlight concurrency primitives, you can use them the next time you encounter situations such as querying DNS records under high concurrency or Redis caching. Let me leave you with a final question. The example above uses the synchronous blocking method Do. Can you use the asynchronous non-blocking method DoChan instead? How about adding a timeout error return function to SingleFlightGet?

As a reminder, using a context object, the error returned is ctx.err ()

You can write down your solution in the message, it is best to attach the source link, welcome to share the article with your more friends, discuss together. Haven’t paid attention to the public number “network management bi” pay close attention to ah, there will be dry goods technology to share every week.