In the package go
of sync
, there is a singleflight
package, which contains a singleflight.go
file, the code plus comments, a total of 200 lines. The content includes the following pieces:
Group
The structure manages a group of related function call work, it contains a mutex and amap
,map
iskey
the name of the function,value
is the correspondingcall
structure.call
The structure represents ainflight
or completed function call, including waiting componentsWaitGroup
, call resultsval
anderr
, call timesdups
and notification channelschans
.Do
The method receives akey
and functionfn
, it will first checkmap
whether there is alreadykey
a call of this ininflight
, if so, wait and return the existing result, if not, create a new onecall
and execute the function call.DoChan
SimilarDo
but returns achannel
to receive the result.doCall
The method contains the logic of specific processing calls, which will add todefer
andrecover
panic
to distinguish between normalreturn
and before and after the function callruntime.Goexit
.- If it happens
panic
, it will returnpanicwraps
an error to the waiting onechannel
, and if it is,goexit
it will exit directly. Normalreturn
will send results to all notificationschannel
. Forget
The method can forget akey
call, andDo
the function will be re-executed next time.
map
This package implements key
deduplication of the same function calls through mutexes and to avoid double counting of existing calls, and at the same time channel
notifies the caller of the function execution result through the mechanism. In some scenarios that need to ensure a single execution, the methods in this package can be used.
The effect of caching and deduplication can be easily achieved by using singleflight
to avoid repeated calculations. Next, let's simulate the cache penetration scenario that may be caused by concurrent requests, and how to use the package to solve this problem singleflight
:
package main
import (
"context"
"fmt"
"golang.org/x/sync/singleflight"
"sync/atomic"
"time"
)
type Result string
// 模拟查询数据库
func find(ctx context.Context, query string) (Result, error) {
return Result(fmt.Sprintf("result for %q", query)), nil
}
func main() {
var g singleflight.Group
const n = 200
waited := int32(n)
done := make(chan struct{
})
key := "this is key"
for i := 0; i < n; i++ {
go func(j int) {
v, _, shared := g.Do(key, func() (interface{
}, error) {
ret, err := find(context.Background(), key)
return ret, err
})
if atomic.AddInt32(&waited, -1) == 0 {
close(done)
}
fmt.Printf("index: %d, val: %v, shared: %v\n", j, v, shared)
}(i)
}
select {
case <-done:
case <-time.After(time.Second):
fmt.Println("Do hangs")
}
time.Sleep(time.Second * 4)
}
In this program, if the query result is reused, shared
it will return true
, and the penetration query will returnfalse
There is another problem in the above design, that is, when Do is blocked, all requests will be blocked, and memory problems may occur.
At this point, Do
it can be replaced with DoChan
, the implementation of the two is exactly the same, the difference is that the result is returned DoChan()
by channel
. Therefore, you can use select
the statement to achieve timeout control
ch := g.DoChan(key, func() (interface{
}, error) {
ret, err := find(context.Background(), key)
return ret, err
})
// Create our timeout
timeout := time.After(500 * time.Millisecond)
var ret singleflight.Result
select {
case <-timeout: // Timeout elapsed
fmt.Println("Timeout")
return
case ret = <-ch: // Received result from channel
fmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared)
}
Actively return when timeout occurs, without blocking.
At this time, another problem is introduced. Each such request is not highly available, and the success rate cannot be guaranteed. At this time, a certain request saturation can be increased to ensure the final success rate of the business. At this time, one request or multiple requests does not make much difference for downstream services. At this time, it is only used to reduce the order of magnitude of the request, so you can singleflight
use Forget()
To improve the concurrency of downstream requests.
ch := g.DoChan(key, func() (interface{
}, error) {
go func() {
time.Sleep(10 * time.Millisecond)
fmt.Printf("Deleting key: %v\n", key)
g.Forget(key)
}()
ret, err := find(context.Background(), key)
return ret, err
})
Of course, this approach still cannot guarantee 100% success. If a single failure cannot be tolerated, a better processing solution needs to be used in a high-concurrency scenario, such as sacrificing part of real-time performance and fully using cache query + asynchronous update.