Go 使用 MySQL 连接池的正确姿势

sql.DB 是数据库的抽象,它提供了一些跟数据库交互的函数,同时管理维护一个数据库连接池,因此不需要频繁的创建和销毁数据库连接,并且在多个 goroutines 间也是安全的。

在 Go 项目中用 sql.Open 函数创建连接池,可是此时只是初始化了连接池的数据结构,并没有创建任何连接。

连接创建都是 惰性 的,只有当你真正使用到连接的时候,连接池才会创建连接。

连接池很重要,它直接影响着你的程序行为和性能。

连接池

连接池的工作原理:

当你的函数(例如 ExecQuery)调用需要访问底层数据库的时候,函数首先会向连接池请求一个连接。如果连接池有空闲的连接,则返回给函数。否则连接池将会创建一个新的连接给函数。一旦连接给了函数,连接则归属于函数。函数执行完毕后,要不把连接所属权归还给连接池,要么传递给下一个需要连接的(Rows)对象,最后使用完连接的对象也会把连接释放回到连接池。

请求一个连接的函数有好几种,执行完毕处理连接的方式稍有差别,大致如下:

  • db.Ping() 调用完毕后会马上把连接返回给连接池。
  • db.Exec() 调用完毕后会马上把连接返回给连接池,但是它返回的Result对象还保留这连接的引用,当后面的代码需要处理结果集的时候连接将会被重用。
  • db.Query() 调用完毕后会将连接传递给 sql.Rows 类型,当然后者迭代完毕或者显示的调用 Close() 方法后,连接将会被释放回到连接池。
  • db.QueryRow() 调用完毕后会将连接传递给 sql.Row 类型,当 Scan() 方法调用之后把连接释放回到连接池。
  • db.Begin() 调用完毕后将连接传递给 sql.Tx 类型对象,当 Commit()Rollback() 方法调用后释放连接。

因为每一个连接都是惰性创建的,如何验证 sql.Open 调用之后,sql.DB 对象可用呢?通常使用db.Ping()方法初始化,调用了 Ping 之后,连接池一定会初始化一个数据库连接。

关于连接池另外一个知识点就是你不必检查或者尝试处理连接失败的情况。当你进行数据库操作的时候,如果连接失败了,database/sql 会帮你处理。

实际上,当从连接池取出的连接断开的时候,database/sql 会自动尝试重连 10 次。仍然无法重连的情况下会自动从连接池再获取一个或者新建另外一个。

连接池配置

对于连接池的使用依赖于你是如何配置连接池,如果使用不当会导致下面问题:

  1. 大量的空闲连接,导致额外的工作和延迟。
  2. 连接数据库的连接过多导致错误。
  3. 连接数据库的连接太少导致获取连接时阻塞。

在 Go 的标准库 database/sql 中,有几个配置连接池的方法:

SetMaxOpenConns

  • 设置打开数据库的最大连接数。包含正在使用的连接和连接池的连接。
  • 如果你的函数调用需要申请一个连接,并且连接池已经没有了连接或者连接数达到了最大连接数。此时的函数调用将会被 block,直到有可用的连接才会返回。
  • 设置这个值可以避免并发太高导致 too many connections 的错误。
  • 该函数的默认设置是0,表示无限制。

SetMaxIdleConns

  • 设置连接池中的保持连接的最大空闲连接数,不能大于 MaxOpenConns
  • 空闲的连接就是并发时可以立即获取的连接(不需要新建连接)也是用完后放回池里面备用的连接,从而提升性能。
  • 连接每次被使用后,持续空闲时长会被重置,从 0 开始从新计算;
  • 该函数在go 1.18.5sql 标准库中的默认值是 2(文档号称在之后版本可能会调整)

SetConnMaxLifetime

  • 设置一个连接的最长生命周期,连接创建后超过这个时间则过期,而不是变成空闲连接。
  • 这并不能保证连接将在池中一直存活这个周期,很可能由于某种原因连接将变得不可用,并且在此之前自动关闭。
  • sql 标准库实现中,每秒自动运行一次清理操作以便从池中删除“过期”连接。
  • 一般设置成小于数据库本身对连接的超时配置 wait_timeout(MySQL 服务器默认是 8 hour

并发安全

连接池是并发安全的,但连接不是。

如果使用同一个连接,在一个事务里面,不要使用多个 goroutine 去操作这个连接。

连接失效

如果连接是客户端主动关闭的,那会在写包的时候返回 ErrBadConn,连接池会在重试次数内获取新的连接;
如果连接是服务器主动关闭的,客户端并不知道,拿到连接后写包不会报错,但是在读服务器的 response 包的时候会有 unexpected EOF 错误。

连接池状态

使用 Stats 方法了解连接池的基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// DBStats contains database statistics.
type DBStats struct {
MaxOpenConnections int // Maximum number of open connections to the database.

// Pool Status
OpenConnections int // The number of established connections both in use and idle.
InUse int // The number of connections currently in use.
Idle int // The number of idle connections.

// Counters
WaitCount int64 // The total number of connections waited for.
WaitDuration time.Duration // The total time blocked waiting for a new connection.
MaxIdleClosed int64 // The total number of connections closed due to SetMaxIdleConns.
MaxIdleTimeClosed int64 // The total number of connections closed due to SetConnMaxIdleTime.
MaxLifetimeClosed int64 // The total number of connections closed due to SetConnMaxLifetime.
}

使用技巧

  1. 根据经验,你应该显式地设置 MaxOpenConns 值。这个值应该远远低于数据库或者基础设施对连接数的任何硬限制。
  2. MaxIdleConns 应该小于等于 MaxOpenConns
  3. 对于小型的 web 应用程序,推荐配置:

    1
    2
    3
    db.SetMaxOpenConns(64)
    db.SetMaxIdleConns(64)
    db.SetConnMaxLifetime(5*time.Minute)

参考

彦祖老师 wechat