db.begin()不能在handler里反复调用,否则会导致连接池被绕过、事务悬挂及panic未捕获;应仅在业务入口显式开启,配对defer tx.rollback()与tx.commit(),或使用db.transaction闭包确保自动恢复。

db.Begin() 不能在 handler 里反复调用
很多人把 tx := db.Begin() 直接写在 Gin 或 HTTP handler 里,每来一个请求就开一个新事务。这看似“每个请求独立”,实则埋下三重隐患:事务对象不复用、底层连接池被绕过、panic 未捕获时事务悬挂。GORM 1.20 虽会自动捕获回调内 panic 并转为 error,但 handler 层自己写的 Begin() 仍需手动 recover + Rollback(),漏掉一次就可能卡住连接。
正确做法是只在业务逻辑入口(比如 service 方法)中显式开启,并确保 defer tx.Rollback() 和最终 tx.Commit() 成对出现。若用闭包式事务(db.Transaction(func(tx *gorm.DB) error {...})),GORM 会自动处理 recover 和回滚,更安全。
- 别在中间件或路由层调
Begin(),它不属于基础设施层职责 - 事务生命周期必须严格绑定到单次业务操作,不是单次 HTTP 请求——比如一个下单接口含创建订单 + 扣库存 + 发通知,这三步才是一次事务边界
- 用
context.WithTimeout包裹事务执行,防死锁或慢 SQL 拖垮整个连接池
嵌套事务不是真嵌套,DisableNestedTransaction 要慎设
GORM 的“嵌套事务”本质是 savepoint(保存点),不是数据库原生的子事务。当你在已有事务内再调 db.Begin(),GORM 默认会创建 savepoint;若设了 DisableNestedTransaction: true,则直接 panic 报错“no active transaction”。这在高并发下容易误判——你以为关掉嵌套能提升性能,结果只是把错误提前暴露出来。
真正影响并发吞吐的是隔离级别和锁粒度,不是是否允许 savepoint。MySQL 默认 Repeatable Read 下,SELECT ... FOR UPDATE 会锁住间隙,大量并发更新同一范围主键时极易触发锁等待超时。
- 不要为“看起来更干净”而禁用嵌套事务,savepoint 对异常分支回滚很实用
- 需要强一致性且写冲突多的场景,改用
Read Committed隔离级别(通过Session(&gorm.Session{Context: ctx})传 context 并在 SQL 层显式SET TRANSACTION ISOLATION LEVEL READ COMMITTED) - 高频更新字段(如库存)务必加数据库行级锁,且避免
UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock >= 1这类乐观锁语句在无索引条件下全表扫描
Session 配置决定并发行为,SkipDefaultTransaction 不等于跳过风险
Session(&gorm.Session{SkipDefaultTransaction: true}) 常被当作“提升读性能”的银弹,但它只是跳过 GORM 自动包装的默认事务 wrapper,并不改变底层 *sql.DB 的连接复用逻辑。如果此时你又没控制好连接池参数,高并发查询照样打满 max_connections。
更隐蔽的问题是:跳过默认事务后,所有操作都变成 autocommit 模式。一旦某条 UPDATE 因网络中断失败,你无法回滚前面已成功的 INSERT,数据一致性彻底失控。这不是性能优化,是主动放弃 ACID 保障。
- 读多写少且对一致性要求极低的报表类接口,可考虑
SkipDefaultTransaction,但必须搭配Read Committed隔离级别 + 显式连接池限流 - 任何涉及写操作的路径,哪怕只是
UpdateColumns,都应保留在事务内 -
PrepareStmt: true在高并发下值得开——预编译能减少 SQL 解析开销,但要注意 MySQL 服务端max_prepared_stmt_count限制,避免 prepare 泄露
事务超时与连接泄漏比锁竞争更致命
线上最常导致事务失败的,不是死锁日志里明晃晃的 “Deadlock found”,而是悄无声息的连接泄漏:某个事务开了没关、某个 db.Ping() 校验失败后没重试、某个 context 超时后 goroutine 卡在 tx.Commit() 等待锁释放……这些都会让空闲连接数持续下降,db.DB().Stats().OpenConnections 逐渐逼近 SetMaxOpenConns 上限,后续请求直接阻塞在获取连接阶段。
验证方式很简单:SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND != 'Sleep'(MySQL)或 SELECT * FROM pg_stat_activity WHERE state = 'active'(PostgreSQL),看活跃事务是否远超 QPS 合理值。
- 每个事务必须有明确的超时控制,推荐用
context.WithTimeout(ctx, 3*time.Second)包裹整个事务块 - 启动时强制调一次
db.PingContext(),失败立即 panic,别让服务带病上线 - 别依赖
SetConnMaxLifetime清理“坏连接”——它只管老化,不管已卡死的事务;真正要监控的是db.DB().Stats().InUse和.Idle比值,长期InUse接近MaxOpen就说明有泄漏











