Go 语言数据库的实现原理

9.3 数据库

数据库几乎是所有 Web 服务不可或缺的一部分,在所有类型的数据库中,关系型数据库是我们在想要持久存储数据时的首要选择,不过因为关系型数据库的种类繁多,所以 Go 语言的标准库 database/sql 就为访问关系型数据提供了通用的接口,这样不同数据库只要实现标准库中的接口,应用程序就可以通过标准库中的方法访问。

9.3.1 设计原理

结构化查询语言(Structured Query Language、SQL)是在关系型数据库系统中使用的领域特定语言(Domain-Specific Language、DSL),它主要用于处理结构化的数据1。作为一门领域特定语言,它由更加强大的表达能力,与传统的命令式 API 相比,它能够提供两个优点:

  1. 可以使用单个命令在数据库中访问多条数据;
  2. 不需要在查询中指定获取数据的方法;

所有的关系型数据库都会提供 SQL 作为查询语言,应用程序可以使用相同的 SQL 查询在不同数据库中查询数据,当然不同的数据库在实现细节和接口上还略有一些不同,这些不兼容的特性在不同数据库中仍然无法通用,例如:PostgreSQL 中的几何类型,不过它们基本都会兼容标准的 SQL 查询以方便应用程序接入:

sql-and-database

图 9-12 SQL 和数据库

如上图所示,SQL 是应用程序和数据库之间的中间层,应用程序在多数情况下都不需要关心底层数据库的实现,它们只关心 SQL 查询返回的数据。

Go 语言的 database/sql 就建立在上述前提下,我们可以使用相同的 SQL 语言查询关系型数据库,所有关系型数据库的客户端都需要实现如下所示的驱动接口:

type Driver interface {
	Open(name string) (Conn, error)
}

type Conn interface {
	Prepare(query string) (Stmt, error)
	Close() error
	Begin() (Tx, error)
}

database/sql/driver.Driver 接口中只包含一个 Open 方法,该方法接收一个数据库连接串作为输入参数并返回一个特定数据库的连接,作为参数的数据库连接串是数据库特定的格式,这个返回的连接仍然是一个接口,整个标准库中的全部接口可以构成如下所示的树形结构:

database-sql-driver

图 9-13 数据库驱动树形结构

MySQL 的驱动 go-sql-driver/mysql 就实现了上图中的树形结构,我们就可以使用语言原生的接口在 MySQL 中查询或者管理数据。

9.3.2 驱动接口

我们在这里从 database/sql 标准库提供的几个方法为入口分析这个中间层的实现原理,其中包括数据库驱动的注册、获取数据库连接和查询数据,这些方法都是我们在与数据库打交道时的最常用接口。

database/sql 中提供的 database/sql.Register 方法可以注册自定义的数据库驱动,这个 package 的内部包含两个变量,分别是 drivers 哈希以及 driversMu 互斥锁,所有的数据库驱动都会存储在这个哈希中:

func Register(name string, driver driver.Driver) {
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil {
		panic("sql: Register driver is nil")
	}
	if _, dup := drivers[name]; dup {
		panic("sql: Register called twice for driver " + name)
	}
	drivers[name] = driver
}

MySQL 驱动会在 init 中调用上述方法将实现 database/sql/driver.Driver 接口的结构体注册到全局的驱动列表中:

func init() {
	sql.Register("mysql", &MySQLDriver{})
}

当我们在全局变量中注册了驱动之后,就可以使用 database/sql.Open 方法获取特定数据库的连接。在如下所示的方法中,我们通过传入的驱动名获取 database/sql/driver.Driver 组成 database/sql.dsnConnector 结构体后调用 database/sql.OpenDB

func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}
	...
	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

database/sql.OpenDB 函数会返回一个 database/sql.DB 结构体,这是标准库包为我们提供的关键结构体,无论是我们直接使用标准库查询数据库,还是使用 GORM 等 ORM 框架都会用到它:

func OpenDB(c driver.Connector) *DB {
	ctx, cancel := context.WithCancel(context.Background())
	db := &DB{
		connector:    c,
		openerCh:     make(chan struct{}, connectionRequestQueueSize),
		lastPut:      make(map[*driverConn]string),
		connRequests: make(map[uint64]chan connRequest),
		stop:         cancel,
	}
	go db.connectionOpener(ctx)
	return db
}

这个结构体 database/sql.DB 在刚刚初始化时不会包含任何的数据库连接,它持有的数据库连接池会在真正应用程序申请连接时在单独的 Goroutine 中获取。database/sql.DB.connectionOpener 方法中包含一个不会退出的循环,每当该 Goroutine 收到了请求时都会调用 database/sql.DB.openNewConnection

func (db *DB) openNewConnection(ctx context.Context) {
	ci, _ := db.connector.Connect(ctx)
	...
	dc := &driverConn{
		db:         db,
		createdAt:  nowFunc(),
		returnedAt: nowFunc(),
		ci:         ci,
	}
	if db.putConnDBLocked(dc, err) {
		db.addDepLocked(dc, dc)
	} else {
		db.numOpen--
		ci.Close()
	}
}

数据库结构体 database/sql.DB 中的链接器是实现了 database/sql/driver.Connector 类型的接口,我们可以使用该接口创建任意数量完全等价的连接,创建的所有连接都会被加入连接池中,MySQL 的驱动在 connector.Connect 方法实现了连接数据库的逻辑。

无论是使用 ORM 框架还是直接使用标准库,当我们在查询数据库时都会调用 database/sql.DB.Query 方法,该方法的入参就是 SQL 语句和 SQL 语句中的参数,它会初始化新的上下文并调用 database/sql.DB.QueryContext

func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
	var rows *Rows
	var err error
	for i := 0; i < maxBadConnRetries; i++ {
		rows, err = db.query(ctx, query, args, cachedOrNewConn)
		if err != driver.ErrBadConn {
			break
		}
	}
	if err == driver.ErrBadConn {
		return db.query(ctx, query, args, alwaysNewConn)
	}
	return rows, err
}

database/sql.DB.query 函数的执行过程可以分成两个部分,首先调用私有方法 database/sql.DB.conn 获取底层数据库的连接,数据库连接既可能是刚刚通过连接器创建的,也可能是之前缓存的连接;获取连接之后调用 database/sql.DB.queryDC 在特定的数据库连接上执行查询:

func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
	queryerCtx, ok := dc.ci.(driver.QueryerContext)
	var queryer driver.Queryer
	if !ok {
		queryer, ok = dc.ci.(driver.Queryer)
	}
	if ok {
		var nvdargs []driver.NamedValue
		var rowsi driver.Rows
		var err error
		withLock(dc, func() {
			nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
			if err != nil {
				return
			}
			rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
		})
		if err != driver.ErrSkip {
			if err != nil {
				releaseConn(err)
				return nil, err
			}
			rows := &Rows{
				dc:          dc,
				releaseConn: releaseConn,
				rowsi:       rowsi,
			}
			rows.initContextClose(ctx, txctx)
			return rows, nil
		}
	}
	...
}

上述方法在准备了 SQL 查询所需的参数之后,会调用 database/sql.ctxDriverQuery 方法完成 SQL 查询,我们会判断当前的查询上下文究竟实现了哪个接口,然后调用对应接口的 Query 或者 QueryContext

func ctxDriverQuery(ctx context.Context, queryerCtx driver.QueryerContext, queryer driver.Queryer, query string, nvdargs []driver.NamedValue) (driver.Rows, error) {
	if queryerCtx != nil {
		return queryerCtx.QueryContext(ctx, query, nvdargs)
	}
	dargs, err := namedValueToValue(nvdargs)
	if err != nil {
		return nil, err
	}
	...
	return queryer.Query(query, dargs)
}

对应的数据库驱动会真正负责执行调用方输入的 SQL 查询,作为中间层的标准库可以不在乎具体的实现,抹平不同关系型数据库的差异,为用户程序提供统一的接口。

9.3.3 总结

Go 语言的标准库 database/sql 是一个抽象层的经典例子,虽然关系型数据库的功能相对比较复杂,但是我们仍然可以通过定义一系列构成树形结构的接口提供合理的抽象,这也是我们在编写框架和中间层时应该注意的,即面向接口编程 —— 只依赖抽象的接口,不要依赖具体的实现。


  1. Wikipedia: SQL https://en.wikipedia.org/wiki/SQL ↩︎

wechat-account-qrcode

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。