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 导致的事务泄漏。
database/sql 事务获取与连接池交互流程 database/sql 事务获取与连接池交互流程 应用代码 DB.BeginTx(ctx, opts) 连接池 (Pool) 空闲连接 活跃连接 获取 *sql.Conn 创建 *sql.Tx tx.Exec / tx.Query Commit / Rollback 释放连接回池 事务独占一个连接 直到 Commit/Rollback 才归还

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 直接执行的操作都不属于当前事务,可能导致部分提交、部分回滚的灾难性后果。