golang鏈接mysql操做及動態鏈接池設置

golang自己沒有提供鏈接mysql的驅動,可是定義了標準接口供第三方開發驅動。這裏鏈接mysql可使用第三方庫,第三方庫推薦使用https://github.com/Go-SQL-Driver/MySQL這個驅動,更新維護都比較好。下面演示下具體的使用,完整代碼示例能夠參考最後。mysql

下載驅動

sudo go get github.com/go-sql-driver/mysql

數據庫鏈接

db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname?charset=utf8")

其中鏈接參數能夠有以下幾種形式:一般咱們都用第二種。git

user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname

插入操做

stmt, err := db.Prepare(`INSERT user (user_name,user_age,user_sex) values (?,?,?)`)
checkErr(err)
res, err := stmt.Exec("tony", 20, 1)
checkErr(err)
id, err := res.LastInsertId()
checkErr(err)
fmt.Println(id)

這裏使用結構化操做,不推薦使用直接拼接sql語句的方法。github

查詢操做

rows, err := db.Query("SELECT * FROM user")
checkErr(err)

for rows.Next() {
    var userId int
    var userName string
    var userAge int
    var userSex int
    rows.Columns()
    err = rows.Scan(&userId, &userName, &userAge, &userSex)
    checkErr(err)
    fmt.Println(userId)
    fmt.Println(userName)
    fmt.Println(userAge)
    fmt.Println(userSex)
}

這裏查詢的方式使用聲明4個獨立變量userId、userName、userAge、userSex來保存查詢出來的每一行的值。在實際開發中一般會封裝數據庫的操做,對這樣的查詢一般會考慮返回字典類型。golang

//構造scanArgs、values兩個數組,scanArgs的每一個值指向values相應值的地址
columns, _ := rows.Columns()
scanArgs := make([]interface{}, len(columns))
values := make([]interface{}, len(columns))
for i := range values {
    scanArgs[i] = &values[i]
}

for rows.Next() {
    //將行數據保存到record字典
    err = rows.Scan(scanArgs...)
    record := make(map[string]string)
    for i, col := range values {
        if col != nil {
            record[columns[i]] = string(col.([]byte))
        }
    }
    fmt.Println(record)
}

修改操做

stmt, err := db.Prepare(`UPDATE user SET user_age=?,user_sex=? WHERE user_id=?`)
checkErr(err)
res, err := stmt.Exec(21, 2, 1)
checkErr(err)
num, err := res.RowsAffected()
checkErr(err)
fmt.Println(num)

刪除操做

stmt, err := db.Prepare(`DELETE FROM user WHERE user_id=?`)
checkErr(err)
res, err := stmt.Exec(1)
checkErr(err)
num, err := res.RowsAffected()
checkErr(err)
fmt.Println(num)

修改和刪除操做都比較簡單,同插入數據相似,只是使用RowsAffected來獲取影響的數據行數。web

完整代碼

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    insert()
}

//插入demo
func insert() {
    db, err := sql.Open("mysql", "root:@/test?charset=utf8")
    checkErr(err)

    stmt, err := db.Prepare(`INSERT user (user_name,user_age,user_sex) values (?,?,?)`)
    checkErr(err)
    res, err := stmt.Exec("tony", 20, 1)
    checkErr(err)
    id, err := res.LastInsertId()
    checkErr(err)
    fmt.Println(id)
}

//查詢demo
func query() {
    db, err := sql.Open("mysql", "root:@/test?charset=utf8")
    checkErr(err)

    rows, err := db.Query("SELECT * FROM user")
    checkErr(err)

    //普通demo
    //for rows.Next() {
    //    var userId int
    //    var userName string
    //    var userAge int
    //    var userSex int

    //    rows.Columns()
    //    err = rows.Scan(&userId, &userName, &userAge, &userSex)
    //    checkErr(err)

    //    fmt.Println(userId)
    //    fmt.Println(userName)
    //    fmt.Println(userAge)
    //    fmt.Println(userSex)
    //}

    //字典類型
    //構造scanArgs、values兩個數組,scanArgs的每一個值指向values相應值的地址
    columns, _ := rows.Columns()
    scanArgs := make([]interface{}, len(columns))
    values := make([]interface{}, len(columns))
    for i := range values {
        scanArgs[i] = &values[i]
    }

    for rows.Next() {
        //將行數據保存到record字典
        err = rows.Scan(scanArgs...)
        record := make(map[string]string)
        for i, col := range values {
            if col != nil {
                record[columns[i]] = string(col.([]byte))
            }
        }
        fmt.Println(record)
    }
}

//更新數據
func update() {
    db, err := sql.Open("mysql", "root:@/test?charset=utf8")
    checkErr(err)

    stmt, err := db.Prepare(`UPDATE user SET user_age=?,user_sex=? WHERE user_id=?`)
    checkErr(err)
    res, err := stmt.Exec(21, 2, 1)
    checkErr(err)
    num, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(num)
}

//刪除數據
func remove() {
    db, err := sql.Open("mysql", "root:@/test?charset=utf8")
    checkErr(err)

    stmt, err := db.Prepare(`DELETE FROM user WHERE user_id=?`)
    checkErr(err)
    res, err := stmt.Exec(1)
    checkErr(err)
    num, err := res.RowsAffected()
    checkErr(err)
    fmt.Println(num)
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

golang go-sql-drive mysql鏈接池的實現

golang內部自帶了鏈接池功能,剛開始接觸golang的時候不瞭解這個,還本身搞了一個 sql.Open的對象管理池,真的很是囧啊。sql

sql.Open函數其實是返回一個鏈接池對象,不是單個鏈接。在open的時候並無去鏈接數據庫,只有在執行query、exce方法的時候纔會去實際鏈接數據庫。在一個應用中一樣的庫鏈接只須要保存一個sql.Open以後的db對象就能夠了,不須要屢次open。數據庫

由於普通程序執行完畢以後資源就會被釋放掉,因此這裏嘗試使用web服務進行演示。數組

開啓web服務

首頁先啓動一個web服務監聽9090端口,比較簡單很少作說明。併發

func startHttpServer() {
    http.HandleFunc("/pool", pool)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

db對象初始化

聲明一個全局的db對象,並進行初始化。socket

var db *sql.DB

func init() {
    db, _ = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")
    db.SetMaxOpenConns(2000)
    db.SetMaxIdleConns(1000)
    db.Ping()
}

鏈接池的實現關鍵在於SetMaxOpenConns和SetMaxIdleConns,其中: 
SetMaxOpenConns用於設置最大打開的鏈接數,默認值爲0表示不限制。 
SetMaxIdleConns用於設置閒置的鏈接數。

設置最大的鏈接數,能夠避免併發過高致使鏈接mysql出現too many connections的錯誤。設置閒置的鏈接數則當開啓的一個鏈接使用完成後能夠放在池裏等候下一次使用。

請求方法

上面開啓http請求設置了請求/pool地址的執行方法

func pool(w http.ResponseWriter, r *http.Request) {
    rows, err := db.Query("SELECT * FROM user limit 1")
    defer rows.Close()
    checkErr(err)

    columns, _ := rows.Columns()
    scanArgs := make([]interface{}, len(columns))
    values := make([]interface{}, len(columns))
    for j := range values {
        scanArgs[j] = &values[j]
    }

    record := make(map[string]string)
    for rows.Next() {
        //將行數據保存到record字典
        err = rows.Scan(scanArgs...)
        for i, col := range values {
            if col != nil {
                record[columns[i]] = string(col.([]byte))
            }
        }
    }

    fmt.Println(record)
    fmt.Fprintln(w, "finish")
}

func checkErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

pool方法就是從user表中查出一條記錄而後存放到map中,最後輸出finish。代碼到這裏就算完了很是簡單,下面來測試一下。首先啓動http服務,而後使用ab進行併發測試訪問:

ab -c 100 -n 1000 'http://localhost:9090/pool'

在數據庫中經過show processlist查看鏈接進程: 
這裏寫圖片描述 
golang數據庫鏈接池

能夠看到有100來個進程。

由於避免了重複建立鏈接,因此使用鏈接池能夠很明顯的提升性能。有興趣的童靴能夠去掉鏈接池代碼本身測試一下。完整代碼以下:

//數據庫鏈接池測試
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "log"
    "net/http"
)

var db *sql.DB

func init() {
    db, _ = sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/test?charset=utf8")
    db.SetMaxOpenConns(2000)
    db.SetMaxIdleConns(1000)
    db.Ping()
}

func main() {
    startHttpServer()
}

func startHttpServer() {
    http.HandleFunc("/pool", pool)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

func pool(w http.ResponseWriter, r *http.Request) {
    rows, err := db.Query("SELECT * FROM user limit 1")
    defer rows.Close()
    checkErr(err)

    columns, _ := rows.Columns()
    scanArgs := make([]interface{}, len(columns))
    values := make([]interface{}, len(columns))
    for j := range values {
        scanArgs[j] = &values[j]
    }

    record := make(map[string]string)
    for rows.Next() {
        //將行數據保存到record字典
        err = rows.Scan(scanArgs...)
        for i, col := range values {
            if col != nil {
                record[columns[i]] = string(col.([]byte))
            }
        }
    }

    fmt.Println(record)
    fmt.Fprintln(w, "finish")
}

func checkErr(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

小結

golang這邊實現的鏈接池只提供了SetMaxOpenConns和SetMaxIdleConns方法進行鏈接池方面的配置。在使用的過程當中有一個問題就是數據庫自己對鏈接有一個超時時間的設置,若是超時時間到了數據庫會單方面斷掉鏈接,此時再用鏈接池內的鏈接進行訪問就會出錯。

packets.go:32: unexpected EOF
packets.go:118: write tcp 192.168.3.90:3306: broken pipe

上面都是錯誤都是go-sql-drive自己的輸出,有的時候還會出現bad connection的錯誤。多請求幾回後鏈接池會從新打開新鏈接這時候就沒有問題了。

相關文章
相關標籤/搜索