golang 事務


tx對象

tx, err := db.Begin()
tx.Exec(query1)
tx.Exec(query2)
tx.commit()

一般查詢使用的是db對象的方法,事務則是使用另外一個對象。

使用db的Begin方法可以創建tx對象。tx對象也有數據庫交互的Query,Exec和Prepare方法。用法和db的相關用法類似。查詢或修改的操作完畢之后,需要調用tx對象的Commit提交或者Rollback方法回滾。
一旦創建了tx對象,事務處理都依賴與tx對象,這個對象會從連接池中取出一個空閑的連接,接下來的sql執行都基於這個連接,直到commit或者rollback調用之后,才會把連接釋放到連接池。
tx事務環境中,只有一個數據庫連接,事務內的Eexc都是依次執行的,事務中也可以使用db進行查詢,但是db查詢的過程會新建連接,這個連接的操作不屬於該事務。
在tx中使用db是錯誤的:
tx, err := db.Begin()
db.Exec(query1)
tx.Exec(query2)
tx.commit()
上述代碼在調用db的Eexc方法的時候,tx會綁定連接到事務中,db則是額外的一個連接,兩者不是同一個事務。需要注意,Begin和Commit方法,與sql語句中的BEGIN或COMMIT語句沒有關系。

事務與連接

創建Tx對象的時候,會從連接池中取出連接,然后調用相關的Exec方法的時候,連接仍然會綁定在改事務處理中。在實際的事務處理中,go可能創建不同的連接,但是那些其他連接都不屬於該事務。例如上面例子中db創建的連接和tx的連接就不是一回事。

事務的連接生命周期從Beigin函數調用起,直到Commit和Rollback函數的調用結束。事務也提供了prepare語句的使用方式,但是需要使用Tx.Stmt方法創建。prepare設計的初衷是多次執行,對於事務,有可能需要多次執行同一個sql。然而無論是正常的prepare和事務處理,prepare對於連接的管理都有點小復雜。因此私以為盡量避免在事務中使用prepare方式。例如下面例子就容易導致錯誤:

tx, _ := db.Begin()
defer tx.Rollback()
stmt, _ tx.Prepare("INSERT ...")
defer stmt.Close()
tx.Commit()
因為stmt.Close使用defer語句,即函數退出的時候再清理stmt,可是實際執行過程的時候,tx.Commit就已經釋放了連接。當函數退出的時候,再執行stmt.Close的時候,連接可能有被使用了。

事務並發

對於sql.Tx對象,因為事務過程只有一個連接,事務內的操作都是順序執行的,在開始下一個數據庫交互之前,必須先完成上一個數據庫交互。例如下面的例子:

rows, _ := db.Query("SELECT id FROM user") 
for rows.Next() {
    var mid, did int
    rows.Scan(&mid)
    db.QueryRow("SELECT id FROM detail_user WHERE master = ?", mid).Scan(&did)
    
}

調用了Query方法之后,在Next方法中取結果的時候,rows是維護了一個連接,再次調用QueryRow的時候,db會再從連接池取出一個新的連接。rows和db的連接兩者可以並存,並且相互不影響。

可是,這樣邏輯在事務處理中將會失效:

rows, _ := tx.Query("SELECT id FROM user")
for rows.Next() {
   var mid, did int
   rows.Scan(&mid)
   tx.QueryRow("SELECT id FROM detail_user WHERE master = ?", mid).Scan(&did)
}
tx執行了Query方法后,連接轉移到rows上,在Next方法中,tx.QueryRow將嘗試獲取該連接進行數據庫操作。因為還沒有調用rows.Close,因此底層的連接屬於busy狀態,tx是無法再進行查詢的。上面的例子看起來有點傻,畢竟涉及這樣的操作,使用query的join語句就能規避這個問題。例子只是為了說明tx的使用問題。
 

實踐

func doSomething(){
    panic("A Panic Running Error")
}

func clearTransaction(tx *sql.Tx){
    err := tx.Rollback()
    if err != sql.ErrTxDone && err != nil{
        log.Fatalln(err)
    }
}


func main() {
    db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?parseTime=true")
    if err != nil {
        log.Fatalln(err)
    }

    defer db.Close()

    tx, err := db.Begin()
    if err != nil {
        log.Fatalln(err)
    }
    defer clearTransaction(tx)

    rs, err := tx.Exec("UPDATE user SET gold=50 WHERE real_name='vanyarpy'")
    if err != nil {
        log.Fatalln(err)
    }
    rowAffected, err := rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(rowAffected)

    rs, err = tx.Exec("UPDATE user SET gold=150 WHERE real_name='noldorpy'")
    if err != nil {
        log.Fatalln(err)
    }
    rowAffected, err = rs.RowsAffected()
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(rowAffected)

    doSomething()

    if err := tx.Commit(); err != nil {
        // tx.Rollback() 此時處理錯誤,會忽略doSomthing的異常
        log.Fatalln(err)
    }

}

我們定義了一個clearTransaction(tx)函數,該函數會執行rollback操作。因為我們事務處理過程中,任何一個錯誤都會導致main函數退出,因此在main函數退出執行defer的rollback操作,回滾事務和釋放連接。

如果不添加defer,只在最后Commit后check錯誤err后再rollback,那么當doSomething發生異常的時候,函數就退出了,此時還沒有執行到tx.Commit。這樣就導致事務的連接沒有關閉,事務也沒有回滾。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM