分页查询默认不加锁,但order by+limit无索引时可能触发间隙锁;游标分页(如where id > ?)可规避锁问题,需确保排序字段有合适索引;count查询在rr级别下可能加共享锁,应避免在事务中连用count与find,优先用覆盖索引或缓存总数。

分页查询本身不加锁,但 ORDER BY + LIMIT 可能触发间隙锁
GORM 的 Find()、Limit()、Offset() 这些方法生成的 SQL 默认是快照读(MVCC),不主动加锁。但一旦你写了 Order("created_at DESC") 且该字段没索引,MySQL 在执行排序时可能回表扫描,InnoDB 就可能对扫描范围内的索引记录加间隙锁(Gap Lock)或临键锁(Next-Key Lock)。这不是 GORM 控制的,是 MySQL 的并发控制策略。
- 显式加
SELECT ... FOR UPDATE或FOR SHARE才会真正持锁;GORM 默认不会加 - 但如果你在事务中做分页查询(比如用
db.Transaction()包裹),又恰好用了Order()+ 范围条件(如Where("status = ?", "pending")),InnoDB 可能为避免幻读而锁住 WHERE 条件匹配的索引区间 - 高频写入场景下,这种隐式锁会加剧阻塞:多个请求同时分页查
created_at > '2026-08-20',可能互相等待间隙锁释放
游标分页能绕过大部分锁问题,但依赖索引设计
用 WHERE id > ? ORDER BY id ASC LIMIT 20 这类游标查询,只要 id 是主键(聚簇索引),MySQL 就只走索引定位,基本不触发间隙锁——因为查询条件精确命中索引值,不需要“防幻读”的区间保护。
- 必须确保排序字段(如
id或created_at)有单独索引或作为联合索引最左前缀 -
created_at单独建索引不够稳:高并发下毫秒级重复时,WHERE created_at >= ?仍可能锁住重复时间戳所在区间 - 更稳妥的是复合索引:
INDEX (created_at, id),游标传created_at = '2026-08-20 10:00:00', id = 12345,再查WHERE created_at > ? OR (created_at = ? AND id > ?) - GORM 不提供原生游标构造,得手写
Where()条件,别依赖Offset()
Count 查询在高并发下可能成为锁瓶颈
db.Model(&User{}).Where(...).Count(&total) 在 MySQL 中本质是 SELECT COUNT(*) FROM ...。如果 WHERE 条件走不到索引,它会扫全表或大范围索引;更麻烦的是,在 RR 隔离级别下,COUNT 可能对扫描到的索引项加共享锁(S 锁),尤其当表有活跃写入时,会和 INSERT/UPDATE 的 X 锁冲突。
- 别在事务里连写
Count()和Find():两次查询之间若有人插入新记录,总数和当前页数据就对不上;而锁还多持了一次 - 对高频读写表,总数可降级处理:缓存 5 秒 TTL 的近似值,或前端不显示总页数(改用“加载更多”按钮)
- 真要准总数,优先用覆盖索引:比如
WHERE status = ?,就在status字段建索引,让 COUNT 走索引统计而非聚簇索引
Preload 关联查询会放大锁影响范围
在分页中用 Preload("Orders"),GORM 先查出 20 个用户,再发一条 SELECT * FROM orders WHERE user_id IN (1,2,...,20)。如果 orders.user_id 没索引,第二条语句可能全表扫描并加大量记录锁;即使有索引,IN 列表里的每个 user_id 都可能触发独立的索引查找与锁。
- 高频写入表慎用 Preload 分页:它把单次分页变成 N+1 次查询,锁粒度从“20 行用户”扩大到“20 组订单”,并发冲突概率指数上升
- 替代方案:分两步查,先分页拿用户 ID 列表,再用
db.Where("user_id IN ?", ids).Order("created_at DESC").Limit(3)查每个用户最新 3 条订单——手动控制关联深度和锁范围 - 永远检查
EXPLAIN输出:确认Preload生成的子查询是否走了索引,有没有 Using filesort 或 Using temporary
游标分页不是“更高级的写法”,而是高频读写场景下避免锁竞争的刚性要求。索引设计错了,再小心写 Where() 也白搭;而 Count 查询的锁开销,常被当成“只是慢一点”,其实它才是压垮数据库的第一张多米诺骨牌。











