★ 连接池与数据库并发:池参数、事务与锁
一句话结论:database/sql(GORM 底层)自带连接池,四个参数各管一件事:SetMaxOpenConns 管上限防打爆 DB、SetMaxIdleConns 管空闲保留、SetConnMaxLifetime 管连接寿命(要小于 MySQL wait_timeout/网关断连时间,防“断连后还在用”)、SetConnMaxIdleTime 管空闲回收。池是阻塞队列:连接被占满时新请求排队等待,超时才报错。事务独占一个连接,所以长事务=长占连接,事务里别做慢查询与外部调用;并发正确性靠两把钥匙:悲观锁(SELECT ... FOR UPDATE)适合写冲突多,乐观锁(version 字段 CAS 更新)适合写冲突少;死锁靠“一致加锁顺序 + 短事务 + 合适索引”预防。
池参数与示例
| 参数 | 作用 | 常见建议 |
|---|---|---|
| SetMaxOpenConns | 同时打开的连接上限 | 按并发×单请求耗时压测确定,别超过 DB max_connections |
| SetMaxIdleConns | 空闲连接保留数 | 高并发服务可 ≈ MaxOpen;默认才 2 |
| SetConnMaxLifetime | 连接最大寿命 | 小于 wait_timeout(MySQL 默认 8h)与 LB 断连时间,如 30~60 分钟 |
| SetConnMaxIdleTime | 空闲多久回收 | 视波动设 5~30 分钟 |
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(50)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
症状识别:DB 报 too many connections→MaxOpen 太大或连接没归还;客户端报“等待连接超时”→MaxOpen 太小或慢查询/长事务占池。
事务与锁
// 悲观锁:事务内锁行,防止并发扣超
err := db.Transaction(func(tx *gorm.DB) error {
var acc Account
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
First(&acc, "id = ?", id).Error; err != nil { return err }
// 此时行被锁,其他事务的 FOR UPDATE 等待
return tx.Model(&acc).Update("balance", acc.Balance-amount).Error
})
// 乐观锁:CAS,谁 version 对上谁成功
res := db.Model(&Order{}).
Where("id = ? AND version = ?", id, oldVersion).
Update("version", oldVersion+1).RowsAffected // 0=被别人改了,重试
- 事务纪律:事务里只做必要 DB 操作;禁止事务内做外部 HTTP、慢查询、等待用户输入;大事务分批。
- 死锁预防:多表更新按固定顺序(如按主键排序)加锁、缩短事务窗口、索引避免全表锁与间隙锁放大;MySQL 死锁会回滚一方(报错 1213),应用要做有限重试。
- 无锁原子扣减更优:UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock >= 1,一条语句解决“先查后写”的竞态。
追问记忆点
- 追问:连接池大小和并发 goroutine 数什么关系?——goroutine 再多,真正同时执行 SQL 的也只有池容量个;池设太小是排队瓶颈,设太大会拖垮 DB,用压测找拐点。
- 追问:为什么事务必须独占连接?——BEGIN/COMMIT 是连接上的会话状态,混用会让事务串台。
- 追问:悲观锁和乐观锁怎么选?——写冲突频繁、一致性要求苛刻选悲观锁;读多写少、冲突概率低选乐观锁(失败重试成本低)。
- 记忆点:四个池参数看上限/空闲/寿命;长事务=占连接;行锁 FOR UPDATE 与 version CAS 二选一;加锁顺序一致防死锁。