GORM默认事务可能脏读,因其复用连接池会话的隔离级别(如MySQL默认REPEATABLE READ、PostgreSQL默认READ COMMITTED),未显式设置Session{IsolationLevel: sql.LevelReadCommitted}则无法保证防脏读;db.Transaction()本身不覆盖连接默认级别,且SQLite会静默忽略隔离级别设置。

为什么默认 GORM 事务仍可能脏读
不是 GORM 本身“允许”脏读,而是它默认复用数据库连接池的会话隔离级别。MySQL 默认是 REPEATABLE READ,PostgreSQL 默认是 READ COMMITTED,但如果你没显式控制事务上下文,GORM 的 db.Transaction() 会继承连接当前的隔离级别——而这个级别可能被其他 goroutine 修改过,或根本没设对。
必须用 Session{IsolationLevel: sql.LevelReadCommitted} 显式指定
仅靠 db.Transaction(func(tx *gorm.DB) error { ... }) 不够,它不会覆盖连接默认隔离级别。正确写法是传入带隔离级别的 gorm.Session:
-
sql.LevelReadCommitted是防脏读的最低有效级别,MySQL/PostgreSQL 都支持 - 别用
sql.LevelRepeatableRead(MySQL 默认),高频更新下易因间隙锁拖慢 TPS - 别依赖
db.Exec("SET TRANSACTION ISOLATION LEVEL ..."),它只作用于当前连接,不保证事务内生效
示例:
err := db.Transaction(func(tx *gorm.DB) error {
var user User
if err := tx.Where("id = ?", 1).First(&user).Error; err != nil {
return err
}
// 此时读到的数据一定是已提交的
return tx.Model(&user).Update("balance", user.Balance+100).Error
}, &gorm.Session{
IsolationLevel: sql.LevelReadCommitted,
})
FOR UPDATE 锁不能替代隔离级别
有人误以为加 Clauses(clause.Locking{Strength: "UPDATE"}) 就能防脏读,其实不是:
-
FOR UPDATE解决的是“本事务写前防止别人改”,不解决“本事务读时看到别人未提交的改” - 如果另一个事务在你
SELECT ... FOR UPDATE前已UPDATE但未COMMIT,你仍可能读到旧值(取决于隔离级别) - 真正防脏读,靠的是隔离级别本身对“未提交数据不可见”的语义保证
连接池配置不当会让隔离级别失效
即使代码里写了 IsolationLevel,若连接池参数混乱,事务可能复用到残留旧状态的连接:
-
db.Config.ConnMaxLifetime = 0或设得过大 → 连接长期存活,可能携带上一个事务遗留的SET SESSION设置 -
db.Config.MaxOpenConns过小 → 多个事务争抢连接,隔离级别切换频繁,状态污染风险升高 - 务必搭配:
db.Config.ConnMaxLifetime = 30 * time.Second+db.Config.MaxIdleConns = 20
最隐蔽的坑是:本地开发用 SQLite 测试通过,上线 MySQL 后突然出现脏读——因为 SQLite 不支持事务隔离级别切换,GORM 会静默忽略 IsolationLevel 参数。











