golang sync.singleflight はホットスポット キャッシュの侵入問題を解決します

goのパッケージには、コードとコメントの合計 200 行のファイルが含まれるパッケージsyncがあります。コンテンツには次の部分が含まれます。singleflightsingleflight.go

  1. Groupこの構造体は、関連する関数呼び出し作業のグループを管理します。これにはミューテックスが含まれておりmap、は関数の名前mapであり、対応する構造体です。keyvaluecall
  2. callこの構造はinflight、待機中のコンポーネントWaitGroup、呼び出し結果、val呼び出しerr時間dups、通知チャネルなど、関数呼び出しまたは完了した関数呼び出しを表しますchans
  3. Doこのメソッドはkeyand functionを受け取り、最初にthis の呼び出しが既に存在するかどうかをfn確認し、存在する場合は待機して既存の結果を返し、存在しない場合は新しい結果を作成して関数呼び出しを実行します。mapkeyinflightcall
  4. DoChan似ていますが、結果を受け取るためにDoを返します。channel
  5. doCallこのメソッドには、関数呼び出しの前後と通常の呼び出しを追加しdeferrecover panic区別する特定の処理呼び出しのロジックが含まれていますreturnruntime.Goexit
  6. これが発生した場合はpanic、待機しているユーザーにpanicwrapsエラーchannel、エラーが発生した場合はgoexit直接終了します。Normal はreturn結果をすべての通知に送信しますchannel
  7. Forgetメソッドはkey呼び出しを忘れることができ、Do関数は次回再実行されます。

このパッケージは、ミューテックスを介した同じ関数呼び出しの重複排除をmap実装し、既存の呼び出しの二重カウントを回避すると同時に、この機構を通じて呼び出し元に関数の実行結果を通知します。単一の実行を保証する必要がある一部のシナリオでは、このパッケージのメソッドを使用できます。keychannel

キャッシュと重複排除の効果は、計算の繰り返しを避けるためにを使用することで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% の成功を保証することはできません。単一の失敗が許容できない場合は、リアルタイム パフォーマンスの一部を犠牲にしてキャッシュ クエリと完全に使用するなど、同時実行性の高いシナリオでより優れた処理ソリューションを使用する必要があります。非同期更新。

おすすめ

転載: blog.csdn.net/w_monster/article/details/131998163