context使用不當(dāng)引發(fā)的一個(gè)bug
背景
今天與大家分享一個(gè)日常開發(fā)比較容易錯(cuò)誤的點(diǎn),那就是
contxt誤用導(dǎo)致的bug,我自己就因?yàn)檎`用導(dǎo)致異步更新緩存都失敗了,究竟是因?yàn)槭裁茨兀靠催@樣一個(gè)例子,光看代碼,你能看出來有什么bug嗎?
func AsyncAdd(run func() error) {
//TODO: 扔進(jìn)異步協(xié)程池
go run()
}
func GetInstance(ctx context.Context,id uint64) (string, error) {
data,err := GetFromRedis(ctx,id)
if err != nil && err != redis.Nil{
return "", err
}
// 沒有找到數(shù)據(jù)
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
}
}
分析
我們先簡單分析一下,這一段代碼要干什么?其實(shí)很簡單,我們想要獲取一段信息,首先會從緩存中獲取,如果緩存中獲取不到,我們就從DB中獲取,從DB中獲取到信息后,在協(xié)程池中放入更新緩存的方法,異步去更新緩存。整個(gè)設(shè)計(jì)是不是很完美,但是在實(shí)際工作中,異步更新緩存就沒有成功過?
導(dǎo)致失敗的原因就在這一段代碼:
AsyncAdd(func() error{
return UpdateCache(ctx,id,data)
})
錯(cuò)誤的原因只有一個(gè),就是這個(gè)ctx,如果改成這樣,就啥事沒有了。
AsyncAdd(func() error{
ctxAsync,cancel := context.WithTimeout(context.Background(),3 * time.Second)
defer cancel()
return UpdateCache(ctxAsync,id,data)
})
看到這個(gè),想必大家就已經(jīng)知道為什么吧?
在這個(gè)ctx樹中,根結(jié)點(diǎn)發(fā)生了cancel(),會將信號即時(shí)同步給下層,因?yàn)楫惒饺蝿?wù)的ctx也在這棵樹的節(jié)點(diǎn)上,所以當(dāng)main goroutine取消了ctx時(shí),異步任務(wù)也被取消了,導(dǎo)致了緩存更新一直失敗。

因?yàn)槲抑皩戇^一篇關(guān)于詳解Context包,看這一篇就夠了!!!的文章,就不在這里細(xì)說其原理了,想知道其內(nèi)部是怎么實(shí)現(xiàn)的,看以前這篇文章就可以了。在這里在與大家分享一下context的使用原則,避免踩坑。
context.Background 只應(yīng)用在最高等級,作為所有派生 context 的根。 context 取消是建議性的,這些函數(shù)可能需要一些時(shí)間來清理和退出。 不要把 Context放在結(jié)構(gòu)體中,要以參數(shù)的方式傳遞。以 Context作為參數(shù)的函數(shù)方法,應(yīng)該把Context作為第一個(gè)參數(shù),放在第一位。給一個(gè)函數(shù)方法傳遞Context的時(shí)候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO Context的Value相關(guān)方法應(yīng)該傳遞必須的數(shù)據(jù),不要什么數(shù)據(jù)都使用這個(gè)傳遞。context.Value 應(yīng)該很少使用,它不應(yīng)該被用來傳遞可選參數(shù)。這使得 API 隱式的并且可以引起錯(cuò)誤。取而代之的是,這些值應(yīng)該作為參數(shù)傳遞。 Context是線程安全的,可以放心的在多個(gè)goroutine中傳遞。同一個(gè)Context可以傳給使用其的多個(gè)goroutine,且Context可被多個(gè)goroutine同時(shí)安全訪問。 Context 結(jié)構(gòu)沒有取消方法,因?yàn)橹挥信缮?context 的函數(shù)才應(yīng)該取消 context。
Go 語言中的 context.Context 的主要作用還是在多個(gè) Goroutine 組成的樹中同步取消信號以減少對資源的消耗和占用,雖然它也有傳值的功能,但是這個(gè)功能我們還是很少用到。在真正使用傳值的功能時(shí)我們也應(yīng)該非常謹(jǐn)慎,使用 context.Context 進(jìn)行傳遞參數(shù)請求的所有參數(shù)一種非常差的設(shè)計(jì),比較常見的使用場景是傳遞請求對應(yīng)用戶的認(rèn)證令牌以及用于進(jìn)行分布式追蹤的請求 ID。
總結(jié)
寫這篇文章的目的,就是把我日常寫的bug分享出來,防止后人踩坑。已經(jīng)踩過的坑就不要再踩了,把找bug的時(shí)間節(jié)省出來,多學(xué)點(diǎn)其他知識,他不香嘛~。
推薦閱讀
