如何正确回滚事务?

问题描述 投票:0回答:2

我正在尝试了解如何在 goland 中处理交易,并查看了许多不同的教程,其中以不同的方式安排了关闭交易。我有几个问题没有明确的答案。

(1) 据我了解,

defer tx.Rollback()
将在
tx.Commit()
之后被调用,所以
rollback
在这种情况下不会做任何事情?这样做有多好?

func WithTx(ctx context.Context) (err error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    //(1)
    defer tx.Rollback()

    // work

    if err = tx.Commit(); err != nil {
        return err
    }

    return nil
}

(2) 如果操作过程中可能出现

recover
,那么使用
panic
有多好?我是否理解正确,如果有
error
,那么
recove
r将不会被执行(r == nil)`?

(3)提交过程中出现错误是否需要回滚?

(4)如何将交易转为延期更正确?

defer func() {tx.Rollback()}
defer func(ttx *sql.Tx) {ttx.Rollbac()}(tx)

func WithTx(ctx context.Context) (err error) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }

    defer func() {
        //(2)
        if r := recover(); r != nil {
            _ = tx.Rollback()
            err = fmt.Errorf("exception")
        } else if err != nil {
            _ = tx.Rollback()
        } else {
        //(3)
        if errCommit = tx.Commit(); err != nil {
           _ = tx.Rollback()
           err = errCommit 
          }
        }
    }() //(4)

    // work with panic
    panic("")

    
    return nil
}

也许有一些关于如何正确回滚事务的常见做法?

go
2个回答
0
投票

大部分需要说的内容都包含在评论中,但让我们回顾一下:

  1. panic
    !=
    error
    。不要惊慌,更不用说如果所说的惊慌用于指示发生了可恢复的“错误”。返回错误 参见上文:在这种情况下使用
  2. panic
  3. recover
    不是
    一个好主意。这充其量是糟糕的风格。您正在滥用 panic-
    recover
    机制来实现一些隐约让人想起
    try-catch
     的东西
    
    取决于数据库。如果您无法提交交易,那么可能没有什么可以回滚的。检查您的数据库的文档。无论如何,您都会忽略
  4. Rollback
  5. 调用返回的任何错误,并且在大多数情况下,如果提交失败,则回滚调用基本上是一个空操作,因此即使不需要它,也可以使用它。
    两者在功能上是等效的。如果您有一堆带有多个事务的延迟调用,则后者可能是您需要做的事情,但对我来说,这表明您的函数本身需要重构/清理。
  6. 所以
IF

如果发生错误你想回滚事务,而 panic 不是这样做的方法,那么你会怎么做呢?好吧,您可以在出现错误时对其进行处理,并随时退出/回滚:

func (s *Storage) FooTx(ctx context.Context, args ...any) error {
    tx, err := s.db.BeginTx(ctx)
    if err != nil {
        return err
    }
    for _, arg := range args {
        // perform some logic/prep work on the input
        query, err := s.processFoo(arg)
        if err != nil {
            tx.Rollback()
            return err
        }
        // execute query for this argument, rollback if it fails
        if err := tx.Exec(query, arg); err != nil {
            tx.Rollback()
            return err
        }
    }
    return tx.Commit()
    // or
    if err := tx.Commit(); err != nil {
        tx.Rollback()
        return err
    }
    return nil
}

现在这有点冗长,但很容易理解。我们可以使代码更加简洁,但代价是不太具体的错误处理和可读性:

// note: named return value func (s *Storage) FooTx(ctx context.Context, args ...any) (err error) { tx, err := s.db.BeginTx(ctx) if err != nil { return err } // now we have a transaction, if an error happens here, we assume a rollback is required defer func() { if err != nil { tx.Rollback() return } err = tx.Commit() // commit, set return error here }() var query string for _, arg := range args { // note: we're using =, not :=. We are assigning to the return variable named err if query, err = s.processFoo(arg); err != nil { return // err is set and not nil, the defer function will handle it } if err = tx.Exec(query, arg); err != nil { return // again, err is assigned, the defer func will roll back the TX } } // done, commit is handled in the defer }

如果提交调用失败,你需要回滚,并且你仍然想使用 defer 的东西,那么上面的函数应该这样写:

func (s *Storage) FooTx(ctx context.Context, args ...any) (err error) { tx, err := s.db.BeginTx(ctx) if err != nil { return err } defer func() { if err != nil { tx.Rollback() // if we're returning an error, rollback the transaction } }() var query string for _, arg := range args { if query, err = s.processFoo(arg); err != nil { return } if err = tx.Exec(query, arg); err != nil { return } } return tx.Commit() // sets the return variable to whatever Commit returns }

这应该可行(未测试代码),但就我个人而言,我可能会选择第一个实现。拥有干净、易于阅读的代码比充满 
defer

调用的代码更有价值。如果您开始添加

defer
调用来处理像处理提交和回滚数据库事务这样重要的事情,那么我会倾向于问为什么,并将其标记为代码气味 9/10 次。偶尔
defer
没有什么问题,例如:
mu.Lock()
defer mu.Unlock()
// or
ctx, cfunc := context.WithCance(context.Background())
defer cfunc()

确保您不会忘记释放锁或取消上下文。我确实质疑将逻辑和错误处理放置到延迟函数中的决定,远离错误的来源或导致问题的逻辑。以节省 5-10 行代码为名的混淆通常意味着优先考虑的是错误的事情。


0
投票

重定1.,

defer tx.Rollback()

用于“

执行事务
”和数据库/sql中的示例代码,所以使用起来很安全。此外,在处理数据库连接时,额外调用的影响应该不明显。 这也回答了 3.,因为您可以返回任何错误,并且您的交易将自动回滚。

另请参阅。 4.,如您所见,

defer tx.Rollback()

是最容易阅读和您应该使用的。

考虑到 2. 

recover()

,您应该只恢复可以可靠处理的错误。由于您预计不会出现任何恐慌,因此您不应该处理它们。如果您的进程崩溃,服务器将自动回滚任何打开的事务。

    

© www.soinside.com 2019 - 2024. All rights reserved.