go
のパッケージには、コードとコメントの合計 200 行のファイルが含まれるパッケージsync
があります。コンテンツには次の部分が含まれます。singleflight
singleflight.go
Group
この構造体は、関連する関数呼び出し作業のグループを管理します。これにはミューテックスが含まれておりmap
、は関数の名前map
であり、対応する構造体です。key
value
call
call
この構造はinflight
、待機中のコンポーネントWaitGroup
、呼び出し結果、val
呼び出しerr
時間dups
、通知チャネルなど、関数呼び出しまたは完了した関数呼び出しを表しますchans
。Do
このメソッドはkey
and functionを受け取り、最初にthis の呼び出しが既に存在するかどうかをfn
確認し、存在する場合は待機して既存の結果を返し、存在しない場合は新しい結果を作成して関数呼び出しを実行します。map
key
inflight
call
DoChan
似ていますが、結果を受け取るためにDo
を返します。channel
doCall
このメソッドには、関数呼び出しの前後と通常の呼び出しを追加しdefer
てrecover
panic
区別する特定の処理呼び出しのロジックが含まれています。return
runtime.Goexit
- これが発生した場合は
panic
、待機しているユーザーにpanicwraps
エラーchannel
、エラーが発生した場合はgoexit
直接終了します。Normal はreturn
結果をすべての通知に送信しますchannel
。 Forget
メソッドはkey
呼び出しを忘れることができ、Do
関数は次回再実行されます。
このパッケージは、ミューテックスを介した同じ関数呼び出しの重複排除をmap
実装し、既存の呼び出しの二重カウントを回避すると同時に、この機構を通じて呼び出し元に関数の実行結果を通知します。単一の実行を保証する必要がある一部のシナリオでは、このパッケージのメソッドを使用できます。key
channel
キャッシュと重複排除の効果は、計算の繰り返しを避けるためにを使用することでsingleflight
簡単に実現できます。次に、同時リクエストによって引き起こされる可能性のあるキャッシュ侵入シナリオと、この問題を解決するためにパッケージを使用する方法をシミュレートしましょう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)
}
このプログラムでは、クエリ結果が再利用される場合はshared
を返しtrue
、ペネトレーション クエリは を返します。false
上記の設計には別の問題があります。つまり、Do がブロックされると、すべてのリクエストがブロックされ、メモリの問題が発生する可能性があります。
この時点で、Do
これを に置き換えることができますDoChan
。この 2 つの実装はまったく同じですが、違いは、結果がDoChan()
によって返されることですchannel
。したがって、select
このステートメントを使用してタイムアウト制御を実現できます。
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)
}
タイムアウトが発生した場合、ブロックせずにアクティブに戻ります。
このとき、それぞれのリクエストの可用性が高くなく、成功率が保証できないという別の問題が発生します。現時点では、ビジネスの最終的な成功率を確保するために、特定のリクエストの飽和率を高めることができます。この時点では、1 つのリクエストまたは複数のリクエストは、ダウンストリーム サービスにとって大きな違いはありません。現時点では、リクエストを減らすためにのみ使用されます。リクエストの大きさが桁違いなので、 ダウンストリーム リクエストの同時実行性を向上させるためにsingleflight
使用できますForget()
。
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
})
もちろん、このアプローチでも 100% の成功を保証することはできません。単一の失敗が許容できない場合は、リアルタイム パフォーマンスの一部を犠牲にしてキャッシュ クエリと完全に使用するなど、同時実行性の高いシナリオでより優れた処理ソリューションを使用する必要があります。非同期更新。