2. Go 数据库基础:database/sql 接口、连接池配置、事务对象获取
在构建具备事务边界的 API 之前,必须扎实掌握 Go 标准库 database/sql 的核心抽象。本章将深入剖析接口设计、连接池调优以及事务对象 *sql.Tx 的获取方式,为后续的声明式事务与补偿机制奠定基础。
2.1 database/sql 接口体系
database/sql 是 Go 官方提供的数据库操作抽象层,它定义了一组通用接口,不绑定任何具体数据库驱动。所有主流数据库(PostgreSQL、MySQL、SQLite、SQL Server 等)均通过实现 driver.Driver 接口接入该体系。
| 核心接口 / 类型 | 职责 | 生命周期 |
|---|---|---|
*sql.DB |
数据库连接池的抽象,提供 Query、Exec、Begin 等方法 |
应用全局单例,长期存活 |
*sql.Conn |
从连接池中获取的单一连接,用于需要独占连接的场景 | 使用后必须归还池中 |
*sql.Tx |
事务对象,封装 Commit 和 Rollback 操作 |
事务边界内有效 |
*sql.Stmt |
预编译语句,可绑定到 DB、Conn 或 Tx |
可复用,需关闭 |
*sql.Rows |
查询结果集迭代器,必须 Close 释放连接 |
迭代完成后立即关闭 |
关键认知:
*sql.DB 不是连接,而是连接池管理器。所有通过 sql.Open 返回的 *sql.DB 都是并发安全的,无需在应用层加锁。
2.2 连接池配置与调优
连接池的默认配置通常不适合生产环境。不合理的配置会导致连接耗尽、延迟飙升或资源泄漏。以下四个参数直接决定连接池行为:
- SetMaxOpenConns:最大打开连接数(含正在使用的和空闲的)。默认值 0 表示无限制,生产环境必须设置。
- SetMaxIdleConns:最大空闲连接数。默认值 2,高并发场景建议适当提高。
- SetConnMaxLifetime:连接最大存活时间。建议小于数据库侧的超时时间(如 MySQL 的
wait_timeout)。 - SetConnMaxIdleTime:连接最大空闲时间。超过该时间的空闲连接将被关闭,避免无效连接占用资源。
// 典型生产配置示例(PostgreSQL)
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
db.SetMaxOpenConns(25) // 最大并发连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(30 * time.Minute) // 连接最多存活30分钟
db.SetConnMaxIdleTime(5 * time.Minute) // 空闲超过5分钟则关闭
常见陷阱: 不要将
SetMaxIdleConns 设置得比 SetMaxOpenConns 大,否则空闲连接数永远不会达到设定值。同时,SetConnMaxLifetime 必须小于数据库服务器端的连接超时时间,否则可能产生“断线重连”错误。
2.3 事务对象 (*sql.Tx) 的获取
事务的起点是 DB.Begin() 或 DB.BeginTx()。后者允许传入 context.Context 和 *sql.TxOptions,实现更精细的事务控制。
// 基础事务获取
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback() // 安全兜底:若未 Commit,自动回滚
// 带上下文和隔离级别的事务获取
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
opts := &sql.TxOptions{
Isolation: sql.LevelSerializable, // 隔离级别
ReadOnly: false, // 是否只读
}
tx, err := db.BeginTx(ctx, opts)
if err != nil {
return fmt.Errorf("begin transaction with options: %w", err)
}
defer tx.Rollback()
获取 *sql.Tx 后,所有数据库操作(Exec、Query、QueryRow、Prepare)都必须通过该事务对象调用,而非 *sql.DB。事务内的操作共享同一个底层连接,直到 Commit 或 Rollback 释放该连接回池。
事务边界最佳实践: 始终使用
defer tx.Rollback() 作为安全网。如果事务成功执行 Commit,Rollback 会变成空操作(返回 sql.ErrTxDone),不会造成副作用。这一模式有效防止因早期 return 或 panic 导致的事务泄漏。
2.4 事务隔离级别与 TxOptions
*sql.TxOptions 允许在事务开始时指定隔离级别和只读属性。不同数据库对隔离级别的支持程度不同,Go 标准库定义了以下常量:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 适用场景 |
|---|---|---|---|---|
sql.LevelDefault |
取决于驱动 | 取决于驱动 | 取决于驱动 | 使用数据库默认级别 |
sql.LevelReadUncommitted |
可能 | 可能 | 可能 | 几乎不使用 |
sql.LevelReadCommitted |
否 | 可能 | 可能 | PostgreSQL 默认,多数业务场景 |
sql.LevelRepeatableRead |
否 | 否 | 可能 | MySQL InnoDB 默认,需要一致快照 |
sql.LevelSerializable |
否 | 否 | 否 | 金融、库存等高一致性场景 |
隔离级别选择建议: 在 API 事务边界设计中,优先使用数据库默认隔离级别(通常为 Read Committed 或 Repeatable Read)。仅在明确需要防止特定并发异常时,才提升到 Serializable,但需评估性能开销和死锁风险。
2.5 事务获取的常见错误模式
- 忘记 Rollback: 事务获取后未调用
Rollback或Commit,导致连接泄漏。解决方案:始终使用defer tx.Rollback()。 - 在事务外使用 DB 操作: 事务内的查询应使用
tx.Query而非db.Query,否则操作不在同一事务边界内。 - 嵌套事务误解: Go 标准库不支持嵌套事务。多次调用
Begin会返回错误。如需保存点,需使用数据库原生 SQL(如SAVEPOINT)。 - 长事务: 事务持有连接时间过长,导致连接池耗尽。事务内应避免网络 I/O、外部 API 调用等阻塞操作。
// 错误示例:事务内混用 db 和 tx
tx, _ := db.Begin()
db.Exec("UPDATE accounts SET balance = 100 WHERE id = 1") // 不在事务内!
tx.Exec("UPDATE accounts SET balance = 200 WHERE id = 2")
tx.Commit() // 第一个更新未回滚,造成数据不一致
严格规则: 一旦获取
*sql.Tx,事务边界内的所有数据库操作必须通过该 tx 对象执行。任何通过 db 直接执行的操作都不属于当前事务,可能导致部分提交、部分回滚的灾难性后果。