golang sync.singleflight는 핫스팟 캐시 침투 문제를 해결합니다.

의 패키지 go에는 파일, 코드, 주석 등 총 200줄이 포함된 패키지가 sync있습니다 . 콘텐츠에는 다음과 같은 부분이 포함됩니다.singleflightsingleflight.go

  1. Group구조는 관련 함수 호출 작업 그룹을 관리하며 뮤텍스를 포함하고 함수 이름인 a map해당 map구조 입니다 .keyvaluecall
  2. call구조는 대기 중인 구성 요소 , 호출 결과 , 호출 시간 및 알림 채널을 inflight포함하여 또는 완료된 함수 호출을 나타냅니다 .WaitGroupvalerrdupschans
  3. Do이 메소드는 a key및 function 을 수신하고 먼저 에 이미 this 호출이 있는지 fn확인합니다 . 그렇다면 기다렸다가 기존 결과를 반환하고, 그렇지 않으면 새 결과를 만들고 함수 호출을 실행합니다.mapkeyinflightcall
  4. DoChan유사 Do하지만 channel결과를 받기 위해 a를 반환합니다.
  5. doCall이 메소드에는 함수 호출 전후에 일반 호출 을 추가 defer하고 recover panic구별하는 특정 처리 호출의 논리가 포함되어 있습니다 .returnruntime.Goexit
  6. 그런 일이 발생하면 대기중인 오류를 반환panic 하고 , 오류가 발생하면 바로 종료됩니다. 보통은 모든 알림에 결과를 보냅니다 .panicwrapschannelgoexitreturnchannel
  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두 가지의 구현은 정확히 동일하며 차이점은 결과가 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)
}

타임아웃이 발생하면 차단하지 않고 적극적으로 복귀합니다.

이때 또 다른 문제가 발생하는데, 이러한 요청은 각각 가용성이 높지 않아 성공률을 보장할 수 없습니다. 이때 비즈니스의 최종 성공률을 보장하기 위해 특정 요청 포화도를 높일 수 있습니다. 이때 하나의 요청 또는 여러 요청은 다운스트림 서비스에 큰 차이가 없습니다. 다운스트림 요청의 동시성을 향상시키기 위해 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