以下是踩坑记录。先摆出结论:避免使用上下文通用的ctx来释放资源。
翻车代码
// 入参的ctx本身已经带了超时
func DoSomehting(ctx context.Context, args interface{}) error {
// 使用redis作为分布式锁
isDuplidated, err := redis.SetDeDupliated(ctx, args)
if err != nil {
return err
}
if isDupdated {
return nil
}
// 释放锁. 使用了ctx, 有问题.
defer redis.DeleteDeDuplicated(ctx, args)
// 业务操作
err = doSomethingFoo(ctx, args)
if err != nil {
return err
}
err = doSomethingBar(ctx, args)
if err != nil {
return err
}
return nil
}
入参的ctx带有cancel机制。
问题在于defer那一行代码,释放资源使用了 DoSomething
的ctx。如果业务操作代码cancel了ctx,或者是执行了耗时操作,而正好 redis.DeleteDeDuplicated
也使用了ctx的cancel机制,那么这个redis锁就无法释放了。
如何避免
如果ctx中带有通用的上下文信息,需要写个函数生成一个新的ctx,同时把原来ctx的kv复制出来。否则直接使用context.Background()就好了。
func CopyCtx(ctx context) context.Context {
ret := context.Background()
ret = context.WithValue(ret, ctxKeyFoo, ctx.Value(ctxKeyFoo)
ret = context.WithValue(ret, ctxKeyBar, ctx.Value(ctxKeyBar)
return ret
}
...
defer redis.DeleteDeDuplicated(CopyCtx(ctx), args)
// defer redis.DeleteDeDuplicated(context.Background(), args)
...
思考: ctx的cancel/timeout机制
阅读context代码可以发现,ctx的cancel/timeout机制,对当前ctx以及其子ctx有效,不影响父ctx。
ps: context的父子关系如下
father := context.Background()
son := context.WithValue(father, key, value)
有疑问加站长微信联系(非本文作者)