golang在使用mysql的时候都会用到database/sql

几个问题

• sql 的连接池中的连接是怎么维护的?
• Query/Exec如何获取查询的连接?
• 连接池的连接如何回收/释放的?

重要结构体

DB struct

DB是sql的核心结构,DB是表示零个或多个底层连接池的数据库句柄,是并发安全的。sql.DB不是一个连接,是数据库的抽象接口。它可以根据driver打开关闭数据库连接,管理连接池。所以,如果你没有把连接示释放回连接池,会导致过多连接使系统资源耗尽。

type DB struct {
    driver driver.Driver // 数据库驱动
    dsn    string // 数据库连接参数
    numClosed uint64 // numClosed 是一个原子计数器,表示已关闭连接的总数。Stmt.openStmt 在清除 Stmt.css 中的已关闭连接之前对其进行检查。
    mu           sync.Mutex // 保护下面的字段
    freeConn     []*driverConn // 空闲连接
    connRequests map[uint64]chan connRequest // 阻塞请求队列。当达到最大连接数时,后续请求将插入该队列来等待可用连接
    nextRequest  uint64 // connRequests 的下一个 key
    numOpen      int    // 已连接或者正等待连接的数量
    // 一个创建新连接的信号,
    // 运行connectionOpener()的goroutine读取此chan,maybeOpenNewConnections发送此chan(每个需要的连接发送一次)
    // 它在db.Close()时关闭,并通知connectionOpener goroutine退出。
    openerCh    chan struct{} 
    closed      bool
    dep         map[finalCloser]depSet
    lastPut     map[*driverConn]string // 用于 debug
    maxIdle     int                    // 最大空闲连接数, 0等价于 defaultMaxIdleConns 常量(代码中值为2),负数等价于0
    maxOpen     int                    // 数据库的最大连接数,0 等价于不限制最大连接数
    maxLifetime time.Duration          // 连接的最大生命周期
    cleanerCh   chan struct{} // 用于释放连接池中过期的连接的信号
}

driverConn struct

driverConn使用互斥锁封装一个driver.Conn结构,在所有对Conn的调用期间保持(包括对通过该Conn返回的接口的任何调用)

type driverConn struct {
    db        *DB
    createdAt time.Time
    sync.Mutex  // 保护下面的字段
    ci          driver.Conn
    closed      bool
    finalClosed bool // ci.Close 已经被调用则为 true
    openStmt    map[*driverStmt]bool
    // 下面的字段被 db.mu 保护
    inUse      bool
    onPut      []func() // 下次返回 conn 时运行的代码
    dbmuClosed bool     // 与 closed 字段相同,但由 db.mu 保护,用于 removeClosedStmtLocked
}
// driver.Conn 是具体的接口 用来支持不同的数据库
type Conn interface {
    // Prepare 返回绑定到该连接的就绪语句 Stmt。
    Prepare(query string) (Stmt, error)
    // Close 使当前就绪的语句和事务无效并可能停止,将此连接标记为不再使用。
    //
    // 因为sql包维护一个空闲的连接池,并且只有在空闲连接过剩时才调用Close,所以驱动不需要做自己的连接缓存。
    Close() error
    // Begin 启动并返回一个新的事务 
    Begin() (Tx, error)
}
//释放连接的 调用了DB 的 putConn 方法
func (dc *driverConn) releaseConn(err error) {
    dc.db.putConn(dc, err)
}

驱动注册绑定

我们在使用指定数据库时需要导入github.com/go-sql-driver/mysql包来执行init()函数来注册驱动。
将指定的数据库注册到一个map类型的变量中。

func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    cfg, err := ParseDSN(dsn)
    if err != nil {
        return nil, err
    }
    c := &connector{
        cfg: cfg,
    }
    return c.Connect(context.Background())
}
func init() {
    sql.Register("mysql", &MySQLDriver{})
}

连接

1576583040128-8ec13632-d145-41ca-a800-a7c1ebe28b3b.png

调用sql.Open的时候会启动一个goroutine一直阻塞读取db.oenerCh。当这个openerCh收到信号时,会启动创建连接的流程,调用驱动提供的创建连接的方法创建连接。如果创建成功,优先把该连接给db.connRequests中阻塞的请求,如果没有阻塞的请求就把这个新连接放入db.freeConn中待请求使用。

关键方法

// 调用驱动的 Open 方法创建新连接
func (db *DB) openNewConnection()
// 给阻塞在 connRequest 队列的请求分配连接
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool 

查询方法如何获取连接

两个基本的查询方法Query/Exec

db.Query("select * from lion")
db.Exec("insert into table values(1,'ada')")

• Query:执行需要返回rows的操作,例如(select)不释放连接,但在调用后仍然保持连接,即放回freeConn
• Exec:执行没有rows的操作,例如(insert,update,delete)调用后自动释放连接。

关键方法

// conn 返回新打开的连接,或者从连接池freeConn中取
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error)

连接的回收或释放

执行Query会执行releaseConn方法,会调用putConn方法处理连接

func (dc *driverConn) releaseConn(err error) {
    dc.db.putConn(dc, err)
}
// 把 dc 连接放回连接池 freeConn 或者释放
func (db *DB) putConn(dc *driverConn, err error)

除了上述的连接回收释放方式,我们还可以设置连接的最大存活时间。

func (db *DB) SetConnMaxLifetime(d time.Duration)

具体使用

导入driver

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

连接DB

func test(){
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3303)/database")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

sql.Open的第一个参数是driver名称,第二个参数是driver连接数据库的信息,各个driver可能不同。DB不是连接,并且只有当需要使用时才会创建连接,如果想立即验证连接,可以使用Ping()方法

err = db.Ping()
if err != nil {
    
}

增删改查

查询使用Query方法,增删改使用Exec方法

row, err := db.Query("select * from lion")
defer row.Close()
if err != nil {
    if err == sql.ErrNoRows {
        fmt.Println("没有该id")
    } else {
        log.Fatal(err)
    }
}

db.Query()表示向数据库发送一个query请求,defer row.Close()非常重要,遍历使用row.next(),把遍历的数据存入变量中使用row.Scan(),
结果集(row)为关闭之前,底层连接处于繁忙状态,当遍历到最后一条记录时,会发生一个EOF错误自动调用row.Close(),但如果提前退出循环,row不会关闭,连接不会回到连接池中,连接也不会关闭,手动关闭非常重要row.Close()可以多次调用。

单行Query

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)

事务

数据库操作可以多个连接一起读取但是不能多个连接一起读
db.Begin()开始事务,Commit()或Rollback()关闭事务。

未知Column

row.Columns()使用,用于处理不能得知结果字段个数或类型

    db, err := sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/lion")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    row, err := db.Query("select * from lion")
    defer row.Close()
    if err != nil {
        if err == sql.ErrNoRows {
            fmt.Println("没有该id")
        } else {
            log.Fatal(err)
        }
    }
    cols, err := row.Columns()
    values := make([]sql.RawBytes, len(cols))
    //Scan需要‘[]interface{}’作为参数,所以我们必须将引用复制到这样的切片中
    vals := make([]interface{}, len(values))
    for i := range values {
        vals[i] = &values[i]
    }
    for row.Next() {
        err = row.Scan(vals...)
        if err != nil {
            panic(err.Error())
        }
        var value string
        for i, col := range values {
            if col == nil {
                value = "NULL"
            } else {
                value = string(col)
            }
            fmt.Println(cols[i], ": ", value)
        }
        fmt.Println("-----------------------------------")
    }
Last modification:May 20th, 2020 at 10:31 pm
如果觉得我的文章对你有用,请随意赞赏