MySQL 8.0+中真正的只读事务必须显式执行START TRANSACTION READ ONLY,GORM默认db.Transaction不支持该语义;需用Session{PrepareStmt: false}配合Exec("START TRANSACTION READ ONLY")开启,再通过Clauses(clause.Locking{Strength: Shared})安全加共享锁,并确保查询走索引、字段精简、路由正确。

只读事务在 GORM 中不能靠 db.Transaction 自动开启——MySQL 的 START TRANSACTION READ ONLY 必须显式声明,否则事务仍是可读写的,锁行为、优化器路径、甚至主从路由都可能出错。
怎么开真正的只读事务(MySQL 8.0+)
GORM 默认事务不带 READ ONLY 语义,即使你只做 SELECT,MySQL 仍按读写事务处理:允许加 FOR UPDATE、走写路径优化器、可能被 dbresolver 错判为写操作而切主库。
必须手动注入 READ ONLY 选项:
- 用
Session+PrepareStmt: false避免预编译干扰,再通过Exec显式执行:tx := db.Session(&gorm.Session{PrepareStmt: false}).Begin() tx.Exec("START TRANSACTION READ ONLY") // 后续所有查询都在该只读事务上下文中 var users []User tx.Find(&users) tx.Commit() - 更稳妥的做法是封装成函数,避免每次手写
Exec:func BeginReadOnly(db *gorm.DB) (*gorm.DB, error) { tx := db.Session(&gorm.Session{PrepareStmt: false}).Begin() if err := tx.Exec("START TRANSACTION READ ONLY").Error; err != nil { tx.Rollback() return nil, err } return tx, nil } - 注意:GORM 的
Transaction函数不支持传入READ ONLY参数,硬塞clause或Context都无效;不要试图用db.Clauses(clause.Insert{Modifier: "READ ONLY"}),这不会生效
只读事务下 Shared Lock 怎么安全加
只读事务里想防并发修改(比如核销前校验优惠券剩余量),得加 LOCK IN SHARE MODE,但直接写 Raw 或拼 SQL 极易出错。
正确姿势只有这一种:
- 必须在已开启的只读事务内调用:
tx := BeginReadOnly(db) // 先开 READ ONLY if err != nil { /* handle */ } defer tx.Rollback() <p>var coupon Coupon err := tx.Clauses(clause.Locking{ Strength: clause.LockingStrengthShared, }).Where("id = ? AND remaining > 0", cid).First(&coupon).Error - WHERE 条件必须命中索引,否则 MySQL 升级为表锁;
id主键没问题,但若写成Where("LOWER(code) = ?", code)就会失效 - 不能在非事务上下文里用
Clauses(clause.Locking{...}),MySQL 会忽略锁,且 GORM 不报错 - 别用
Raw("SELECT ... LOCK IN SHARE MODE")——dbresolver无法识别语义,可能发到从库直接报错ERROR 1290
只读场景下的查询优化关键点
只读事务本身不提速,但它是优化的前提:它让 MySQL 能启用只读优化路径(如跳过 undo log 写入、减少锁竞争),配合以下写法才能真正见效:
-
Select显式指定字段,避免SELECT *带来的大字段 IO 和序列化开销:db.Select("id", "name", "status").Where("status = ?", "active").Find(&items) - 用
Preload替代 N+1,但注意只读事务里Preload也走同一事务连接,不会新开连接;若关联表数据量大,考虑分页或延迟加载 - 深分页慎用
OFFSET,改用游标(WHERE id > ? ORDER BY id LIMIT 20),只读事务中游标稳定性更高 - 确保
dbresolver已正确注册,否则只读事务里的Find仍可能静默 fallback 到主库——查不到延迟数据不是 bug,是配置漏了 - 连接池参数要区分主从:
SetMaxOpenConns(100)给从库(读多),SetReadTimeout(10 * time.Second)防慢查询拖垮整个读链路
只读事务的坑不在“开”这个动作,而在后续所有操作是否真落在只读语义上。从 dbresolver 路由、锁声明方式、字段裁剪到索引覆盖,任一环脱节,所谓“只读”就只剩字面意思。


















