tx對象
tx, err := db.Begin()
tx.Exec(query1)
tx.Exec(query2)
tx.commit()
一般查詢使用的是db對象的方法,事務則是使用另外一個對象。
tx, err := db.Begin()
db.Exec(query1)
tx.Exec(query2)
tx.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()
事務並發
對於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) }
實踐
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。這樣就導致事務的連接沒有關閉,事務也沒有回滾。