事务必须显式开启,db.transaction不能省;所有操作须用tx对象,混用原始db将绕过事务;函数返回nil才提交,非nil则自动回滚;preparestmt仅优化性能,不影响一致性。

事务必须显式开启,db.Transaction 不能省
Gin 本身不处理数据一致性,真正起作用的是底层 ORM(比如 GORM)的事务机制。很多新手直接在 handler 里调用 db.Create 或 db.Save,以为“连着同一个 db 实例”就自动事务了——这是错的。GORM 默认所有操作都是自动提交的独立语句,哪怕它们在同一个函数里执行。
正确做法是用 db.Transaction 显式包裹多个操作:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
if err := tx.Model(&user).Where("id = ?", userID).Update("balance", gorm.Expr("balance - ? ", amount)).Error; err != nil {
return err
}
return nil
})
if err != nil {
// 回滚已自动发生,这里只需记录或返回错误
c.AbortWithStatusJSON(500, gin.H{"error": "order failed"})
return
}
- 传入的
tx是事务上下文,所有操作必须用它,不能混用原始db - 函数返回非 nil error 时,GORM 自动回滚;返回 nil 才提交
- 别在事务函数里调用
c.JSON或其他可能 panic 的逻辑,否则可能掩盖真实错误
db.Session(&gorm.Session{PrepareStmt: true}) 对一致性没帮助,别误用
有人看到文档里说 “开启 PrepareStmt 可提升性能”,就以为它能保证多条语句原子性——完全不是一回事。PrepareStmt 只是让 GORM 复用 SQL 预编译句柄,减少解析开销,对事务边界、锁机制、隔离级别毫无影响。
真正影响一致性的配置项是事务隔离级别,例如:
// 读已提交,MySQL 默认
db.Session(&gorm.Session{NewDB: true}).Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
<p>// 或更严格地,在 Transaction 内部指定
err := db.Session(&gorm.Session{IsolationLevel: sql.LevelRepeatableRead}).Transaction(...)
</p>
-
PrepareStmt: true只优化性能,和一致性无关 - 隔离级别需数据库支持,MySQL 中
REPEATABLE READ是默认,但不要盲目设成SERIALIZABLE,会严重拖慢并发 - 跨服务调用(如扣库存 + 发消息)无法靠单库事务保证,得用 Saga 或消息队列补偿
关联写入用 Save 还是 Create?看是否已有主键
当结构体含嵌套关系(比如订单带多个订单项),GORM 的 Save 和 Create 行为差异极大,直接影响外键约束是否生效、是否触发级联插入:
-
db.Create(&order):只插入order本身,忽略order.Items字段,除非模型上明确写了gorm:"foreignKey:OrderID"且启用了Association模式 -
db.Save(&order):若order.ID已存在(非零值),则更新;若为零值,先插入主表,再尝试插入关联项——但前提是关联字段已设置好外键值,且没有启用cascade配置 - 最稳妥方式是手动控制顺序:
db.Create(&order)→ 拿到order.ID→ 给每个item.OrderID = order.ID→db.CreateInBatches(items, 100)
并发更新时 SELECT FOR UPDATE 必须配事务,且不能用 db.First
想防止超卖或重复扣款,常见做法是查余额再扣减。但仅用 db.Where(...).First(&u) + db.Save(&u) 是不安全的——两次操作之间有竞态窗口。必须用行锁,且锁必须落在事务内:
err := db.Transaction(func(tx *gorm.DB) error {
var user User
// 注意:必须用 tx,且加 FOR UPDATE
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&user).Error; err != nil {
return err
}
if user.Balance
-
db.First不支持FOR UPDATE,必须用tx.Clauses(clause.Locking{...}) - 锁只在事务 commit/rollback 后释放,所以整个逻辑必须包在
Transaction里 - 如果用
db.Model(&u).Updates(...)做原子扣减(如balance = balance - ?),可避免查-改两步,更高效也更安全
事务的边界容易被忽略:它不是“函数作用域”,而是 db.Transaction 显式划定的代码块;锁不是“加了就行”,而是必须和事务绑定且由数据库引擎实际生效。这两点出错,表面看代码跑得通,线上却会间歇性出现脏写或超扣。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











