context使用不当引发的一个bug

共 3272字,需浏览 7分钟

 ·

2021-03-27 06:38

背景

今天与大家分享一个日常开发比较容易错误的点,那就是contxt误用导致的bug,我自己就因为误用导致异步更新缓存都失败了,究竟是因为什么呢?看这样一个例子,光看代码,你能看出来有什么bug吗?

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)
  })

错误的原因只有一个,就是这个ctx,如果改成这样,就啥事没有了。

AsyncAdd(func() error{
   ctxAsync,cancel := context.WithTimeout(context.Background(),3 * time.Second)
   defer cancel()
   return UpdateCache(ctxAsync,id,data)
  })

看到这个,想必大家就已经知道为什么吧?

在这个ctx树中,根结点发生了cancel(),会将信号即时同步给下层,因为异步任务的ctx也在这棵树的节点上,所以当main goroutine取消了ctx时,异步任务也被取消了,导致了缓存更新一直失败。

因为我之前写过一篇关于详解Context包,看这一篇就够了!!!的文章,就不在这里细说其原理了,想知道其内部是怎么实现的,看以前这篇文章就可以了。在这里在与大家分享一下context的使用原则,避免踩坑。

  • context.Background 只应用在最高等级,作为所有派生 context 的根。
  • context 取消是建议性的,这些函数可能需要一些时间来清理和退出。
  • 不要把Context放在结构体中,要以参数的方式传递。
  • Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。context.Value 应该很少使用,它不应该被用来传递可选参数。这使得 API 隐式的并且可以引起错误。取而代之的是,这些值应该作为参数传递。
  • Context是线程安全的,可以放心的在多个goroutine中传递。同一个Context可以传给使用其的多个goroutine,且Context可被多个goroutine同时安全访问。
  • Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。

Go 语言中的 context.Context 的主要作用还是在多个 Goroutine 组成的树中同步取消信号以减少对资源的消耗和占用,虽然它也有传值的功能,但是这个功能我们还是很少用到。在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context 进行传递参数请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

总结

写这篇文章的目的,就是把我日常写的bug分享出来,防止后人踩坑。已经踩过的坑就不要再踩了,把找bug的时间节省出来,多学点其他知识,他不香嘛~。


推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报