乐趣区

go-ctx超时导致资源释放失败

以下是踩坑记录。先摆出结论: 避免使用上下文通用的 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)
退出移动版