先看コード:
func AsyncAdd(run func() error) {
//TODO: 非同期のゴルーチンプールに追加
go run()
}
func GetInstance(ctx context.Context,id uint64) (string, error) {
data,err := GetFromRedis(ctx,id)
if err != nil && err != redis.Nil{
return "", err
}
// データが見つからない場合
if err == redis.Nil {
data,err = GetFromDB(ctx,id)
if err != nil{
return "", err
}
AsyncAdd(func() error{
return UpdateCache(ctx,id,data)
})
}
return data,nil
}
func GetFromRedis(ctx context.Context,id uint64) (string,error) {
// TODO: Redisから情報を取得
return "",nil
}
func GetFromDB(ctx context.Context,id uint64) (string,error) {
// TODO: DBから情報を取得
return "",nil
}
func UpdateCache(ctx context.Context,id interface{},data string) error {
// TODO:キャッシュ情報を更新
return nil
}
func main() {
ctx,cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
_,err := GetInstance(ctx,2021)
if err != nil{
return
}
}
原因:誤った使用により非同期更新が失敗する
分析#
まず、このコードの目的を簡単に分析しましょう。非常にシンプルです。情報を取得したい場合、まずキャッシュから取得し、キャッシュに存在しない場合は DB から取得します。DB から情報を取得した後、キャッシュを非同期で更新するために、ゴルーチンプールに更新関数を追加します。この設計は完璧ですが、実際の作業では、非同期のキャッシュ更新はうまくいきませんでした。。。。
失敗の原因は、このコードの一部にあります:
AsyncAdd(func() error{
return UpdateCache(ctx,id,data)
})
エラーの原因は 1 つだけで、この ctx です。これを次のように変更すると、問題は解決します。
AsyncAdd(func() error{
ctxAsync,cancel := context.WithTimeout(context.Background(),3 * time.Second)
defer cancel()
return UpdateCache(ctxAsync,id,data)
})
原因#
元のコードでは、UpdateCache 関数が呼び出される際に外部のコンテキストオブジェクト ctx が使用されており、これにより UpdateCache 関数のタイムアウト時間が GetInstance 関数に渡されたコンテキストオブジェクトに影響を受けています。
匿名関数内で新しいコンテキストオブジェクト ctxAsync を作成し、それを UpdateCache 関数に渡すことで、UpdateCache 関数のタイムアウト時間が外部のコンテキストオブジェクトとは独立していることを保証できます。
また、ctxAsync は匿名関数内で作成されるため、関数の実行が終了した時に cancel () 関数を呼び出して関連するリソースを解放する必要があります。