db.BeginTx 必须绑定未超时的 context:Begin() 不感知上下文超时,事务会一直悬挂;只有 db.BeginTx(ctx, opts) 可控,且 ctx 需在创建前设好超时;GORM 中显式 WithContext 优先于 DefaultTransactionTimeout;子操作需复用同一 ctx;批量写入应分组(1000–5000 行/事务);避免 LIMIT/OFFSET 分页;优先用原生多值 INSERT;高频场景预 Prepare;跨服务需 Saga 模式与发件箱;连接池与 PgBouncer 配置须对齐;务必开启泄漏检测与带超时的健康检查。

db.BeginTx 必须绑定未超时的 context
Go 的 database/sql 中,Begin() 不感知上下文超时,事务会一直挂着直到手动 Commit() 或 Rollback()——哪怕 HTTP 请求早已 504。真正可控的方式只有 db.BeginTx(ctx, opts),且该 ctx 必须在事务创建前就带超时。
- 错误写法:
tx, _ := db.Begin()→ 后续所有ExecContext虽能中断语句,但事务本身不自动回滚 - 正确写法:
ctx, cancel := context.WithTimeout(parentCtx, 15*time.Second),再传给db.BeginTx(ctx, nil) - GORM 用户注意:
DefaultTransactionTimeout仅兜底,一旦显式调用.WithContext(ctx),就完全以该 ctx 为准 - 事务内嵌套子操作(如 Savepoint)也必须复用同一
ctx,不能另起一个
批量写入别全塞进一个事务
几万行数据包进单个 tx.Commit() 看似省事,实际极易触发 WAL 暴涨、锁表、OOM 或主从延迟飙升。事务不是越大越好,而是“够大但可控”。
- 按 1000–5000 行分组,每组单独
BeginTx+Commit,失败只影响本组 - 避免用数据库
LIMIT/OFFSET分页做分块——它会扫描前面所有行;应用层用切片分块更可靠 - 插入多行优先用原生语法:
INSERT INTO t(a,b) VALUES (?,?),(?,?),...,别依赖sql.Named+ struct 切片(底层仍是逐行绑定) - 高频固定结构批量写,提前
db.Prepare()可跳过 SQL 解析,降低服务端压力
跨服务事务不能靠 tx.Commit() 保证一致性
tx.Commit() 在微服务里只管得住本服务的一个 DB 连接。订单服务 commit 成功,不代表库存服务或账户服务也成功——这不是 Go 的 bug,是分布式系统的基本事实。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 放弃“一次全部提交”的幻想,接受最终一致性;Saga 是最贴近 Go 工程实践的落地路径
- 每个正向步骤(如扣库存)必须配套幂等补偿(如加回库存),且补偿接口要带唯一约束或
WHERE status = 'done'条件更新 - 状态进度不能存内存或 Redis;必须落库,字段含
step、compensated_step、version防覆盖 - 消息发送必须和本地事务原子绑定:用发件箱模式(outbox_events 表),由独立 goroutine 轮询投递,失败进 DLQ
连接池与 PgBouncer 配置必须对齐
事务边界失控常源于连接池配置错位。GORM 的 SetMaxOpenConns 和 PgBouncer 的 max_client_conn、pool_size 若不协调,轻则连接等待,重则事务卡死。
立即学习“go语言免费学习笔记(深入)”;
-
SetMaxOpenConns应 ≤ PgBouncer 的max_client_conn,否则部分连接永远拿不到 - 短事务高频场景用 PgBouncer
pool_mode = transaction,事务结束即释放连接 - 务必开启连接泄漏检测:记录每次
db.Conn()的调用栈,对比释放点,定位未 defer 的 conn - 健康检查不能只靠
db.Ping();需定期执行SELECT 1并带 context 控制超时,防止探测请求本身拖垮连接池

















