借 Go 语言 database/sql 包谈数据库驱动和连接池设计

Posted by ChenJY on August 10, 2019 | Viewed times

即使你不了解 Go 语言,阅读本文也不会有障碍

什么是池化技术

池化技术 (Pool) 是一种很常见的编程技巧,在请求量大时能明显优化应用性能,降低系统频繁建连的资源开销。我们日常工作中常见的有数据库连接池、线程池、对象池等,它们的特点都是将 “昂贵的”、“费时的” 的资源维护在一个特定的 “池子” 中,规定其最小连接数、最大连接数、阻塞队列等配置,方便进行统一管理和复用,通常还会附带一些探活机制、强制回收、监控一类的配套功能。

database/sql 包

设计哲学

Go 语言中对数据库进行操作需要借助标准库下的 database/sql 包进行,它对上层应用提供了标准的 API 操作接口,对下层驱动暴露了简单的驱动接口,并在内部实现了连接池管理。这意味着不同数据库的驱动可以很方便地实现这些驱动接口,但不再需要关心连接池的细节,只需要基于单个连接。

极简接口

它对外暴露的接口简单易懂,利于第三方 Driver 去实现,接口的功能包括 Driver 注册、ConnStmtTxRows结果集等,我们通过 ConnStmt 这两个接口来体会一下接口设计的精妙(这两个接口对应到 Java 就是 ConnectionStatement 接口,只是 Go 更加简单)

我相信你即使没有学习过 Go 语言,仅凭你的 Java 知识,也可以毫不费力地看懂上面这些接口的意思,这些对于驱动层暴露的接口非常简单,让驱动程序可以方便地去实现。

调用关系

整个 database/sql 驱动接口的调用关系非常清晰,简单来说驱动程序先通过 Open 方法拿到一个新建的 Conn,然后调用 ConnPrepare 方法,传入 SQL 语句得到该语句的 Stmt,最后调用 StmtExec 方法传入参数返回结果,查询语句同理,但返回的是行数据结果集。

连接池设计

sql.DB 对象关键属性

Go 语言操作数据库时,我们先使用 sql.Open 方法返回一个具体的 sql.DB 对象,如下代码片中的 db

sql.DB 对象即是我们访问数据库的入口,我们看看它里面的关键属性,均与连接池的设计相关

建立连接

事实上,连接并不是在 sql.Open 返回 db 对象时就建立的,这一步仅仅开了个接收建连请求的 channel,实际建连步骤要等到执行具体 SQL 语句时才会进行。下面我们通过一些例子讲述一下连接是怎么建立的,连接池的逻辑又是怎么实现的。

讲述这部分原理不会贴太多的源码,那就变成源码解析了,对不了解 Go 语言的同学也不友好,主要希望能传达一些连接池设计的思想。

database/sql 对上层应用暴露的操作接口中,比较常用的是 ExecQuery,前者常用于执行写 SQL,后者可以用于读 SQL。但是不论走哪个方法,都会调用到建连逻辑 db.conn 方法,附带建连上下文和建连策略两个参数。

其中建连策略分为 cachedOrNewConnalwaysNewConn。前者优先从 freeConn 空闲连接中取出连接,否则就新建一个;后者则永远走新建连接的逻辑。

使用 cachedOrNewConn 策略的建连逻辑中,会先判断是否有空闲连接,如果有取出首个空闲连接,紧接着判断该连接是否过期需要被回收,如果没有过期则可以正常使用进入后续逻辑。如果没有空闲连接则判断连接数是不是已经达到最大,若没有可以新建连接,反之就得阻塞这个请求让它等待可用连接。

如果需要新建连接,则调用底层 Driver 实现的连接器的 Connect 接口,这部分就是由各个数据库 Driver 自行去实现了。

释放连接

某个连接使用完毕之后需要归还给连接池,这也是数据库连接池实现中比较重要的逻辑,通常还伴随着对连接的可靠性检测,如果连接异常关闭,那么不应该继续还给连接池,而是应该新建一个连接进行替换。

JavaDruid 连接池会有 testOnReturn 或者 testOnBorrow 选项,表示在归还连接或者是获取连接时进行有效性检测,但是开启这两项本质上会延长连接被占用的时间,损失一部分性能。Go 语言中对这项功能的实现比较简单,并没有具体的有效性检测机制,只是直接根据连接附带的 err 信息,如果是 ErrBadConn 异常则关闭并发送信号新建一个。

清理连接

database/sql 包下提供了与连接池相关的三个关键参数设置,分别是 maxIdlemaxOpenmaxLifeTime

三个参数的含义很容易理解,如果想要深入了解,推荐阅读 Configuring sql.DB for Better Performance

MySQL 侧会强制 kill 掉长时间空闲的连接(8h),Go 语言提供了 maxLifeTime 选项设置连接被复用的最大时间,注意并不是连接空闲时间,而是从连接建立到这个时间点就会被回收,从而保证连接活性。

这块的清理机制是通过一个异步任务来做的,关键是逻辑是每个一秒遍历检查 freeConn 中的空闲连接,判断是否超出最大复用期限,超出的连接加入 Closing 数组,后续被 Close

总结

最近的工作内容是基于 go-sql-driver 实现了一个支持读写分离和高可用的自定义 driver,在调研和学习期间感受到了 Go 语言 database/sql 包的简明清晰,虽然它在部分功能的实现上偏简单甚至没有,但是依旧覆盖了大部分数据库连接池的主要功能和特性,因此我觉得用它来抛砖引玉是个好选择。


这是一个不定时更新的、披着程序员外衣的文青小号。

在这里,既分享极客技术,也记录人间烟火,欢迎关注。


Comment