golang Mysql -- Tx

Transaction 事務

事務處理是數據的重要特性。尤爲是對於一些支付系統,事務保證性對業務邏輯會有重要影響。golang的mysql驅動也封裝好了事務相關的操做。咱們已經學習了db的Query和Exec方法處理查詢和修改數據庫。mysql

tx對象

通常查詢使用的是db對象的方法,事務則是使用另一個對象。sql.Tx對象。使用db的Begin方法能夠建立tx對象。tx對象也有數據庫交互的Query,Exec和Prepare方法。用法和db的相關用法相似。查詢或修改的操做完畢以後,須要調用tx對象的Commit提交或者Rollback方法回滾。golang

一旦建立了tx對象,事務處理都依賴與tx對象,這個對象會從鏈接池中取出一個空閒的鏈接,接下來的sql執行都基於這個鏈接,直到commit或者rollback調用以後,纔會把鏈接釋放到鏈接池。sql

在事務處理的時候,不能使用db的查詢方法,雖而後者能夠獲取數據,但是這不屬於同一個事務處理,將不會接受commit和rollback的改變,一個簡單的事務例子以下:數據庫

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

在tx中使用db是錯誤的:併發

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

上述代碼在調用db的Eexc方法的時候,tx會綁定鏈接到事務中,db則是額外的一個鏈接,二者不是同一個事務。須要注意,Begin和Commit方法,與sql語句中的BEGIN或COMMIT語句沒有關係。tcp

事務與鏈接

建立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對象,由於事務過程只有一個鏈接,事務內的操做都是順序執行的,在開始下一個數據庫交互以前,必須先完成上一個數據庫交互。例以下面的例子:code

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的使用問題。

實踐

前面對事務解釋了一堆,說了那麼多,其實還不如share的code。下面就事務的使用作簡單的介紹。由於事務是單個鏈接,所以任何事務處理過程的出現了異常,都須要使用rollback,一方面是爲了保證數據完整一致性,另外一方面是釋放事務綁定的鏈接。

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。這樣就致使事務的鏈接沒有關閉,事務也沒有回滾。

總結

database/sql提供了事務處理的功能。經過Tx對象實現。db.Begin會建立tx對象,後者的Exec和Query執行事務的數據庫操做,最後在tx的Commit和Rollback中完成數據庫事務的提交和回滾,同時釋放鏈接。

tx事務環境中,只有一個數據庫鏈接,事務內的Eexc都是依次執行的,事務中也可使用db進行查詢,可是db查詢的過程會新建鏈接,這個鏈接的操做不屬於該事務。

關於database/sql和mysql的驅動,咱們已經分三部份內容介紹了。下一節,將會對以前的內容進行梳理總結,包括錯誤處理和注意事項的補充。

做者:人世間連接:http://www.jianshu.com/p/bc8120bec94e來源:簡書著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索