의 패키지 go
에는 파일, 코드, 주석 등 총 200줄이 포함된 패키지가 sync
있습니다 . 콘텐츠에는 다음과 같은 부분이 포함됩니다.singleflight
singleflight.go
Group
구조는 관련 함수 호출 작업 그룹을 관리하며 뮤텍스를 포함하고 함수 이름인 amap
는 해당map
구조 입니다 .key
value
call
call
구조는 대기 중인 구성 요소 , 호출 결과 , 호출 시간 및 알림 채널을inflight
포함하여 또는 완료된 함수 호출을 나타냅니다 .WaitGroup
val
err
dups
chans
Do
이 메소드는 akey
및 function 을 수신하고 먼저 에 이미 this 호출이 있는지fn
확인합니다 . 그렇다면 기다렸다가 기존 결과를 반환하고, 그렇지 않으면 새 결과를 만들고 함수 호출을 실행합니다.map
key
inflight
call
DoChan
유사Do
하지만channel
결과를 받기 위해 a를 반환합니다.doCall
이 메소드에는 함수 호출 전후에 일반 호출 을 추가defer
하고recover
panic
구별하는 특정 처리 호출의 논리가 포함되어 있습니다 .return
runtime.Goexit
- 그런 일이 발생하면 대기중인 오류를 반환
panic
하고 , 오류가 발생하면 바로 종료됩니다. 보통은 모든 알림에 결과를 보냅니다 .panicwraps
channel
goexit
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
두 가지의 구현은 정확히 동일하며 차이점은 결과가 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% 성공을 보장할 수 없으며, 단일 실패가 허용되지 않는 경우 실시간 성능의 일부를 희생하고 캐시 쿼리를 완전히 사용하는 등 높은 동시성 시나리오에서 더 나은 처리 솔루션을 사용해야 합니다. 비동기 업데이트.