用gorm+hertz做秒杀商品分页列表卡住,因高并发下mysql连接池耗尽、慢查询堆积、redis缓存击穿;应改用游标分页、中间件预校验活动状态、禁用动态排序、加布隆过滤器防穿透。

为什么用 GORM + Hertz 做秒杀商品分页列表会卡住
直接在 Hertz 的 GET /goods 接口里调 db.Find(&list, &cond) 分页查商品,高并发下大概率触发 MySQL 连接池耗尽、慢查询堆积、Redis 缓存击穿三连。秒杀商品不是普通商品——它有强时效性(开抢前/中/后状态不同)、高读频次(每秒数万次刷列表)、低更新容忍(库存变化必须实时可见)。GORM 默认的 Limit/Offset 分页在数据量大时会全表扫描,OFFSET 10000 就可能拖慢到 800ms+,而秒杀页面要求首屏渲染
商品分页必须走 Redis + 预生成 ID 列表
数据库只负责写入和最终一致性,分页读绝对不能打到 MySQL。真实线上做法是:在秒杀活动创建时,用后台任务把该场次所有可售商品 ID 按热度或规则排序,生成一个固定长度的 seckill:goods:list:{activityId} 有序集合(ZSET)或列表(LIST),TTL 设为活动结束时间 + 2 小时。前端分页请求只查 Redis:
-
ZRANGE seckill:goods:list:1001 0 19 WITHSCORES(拉第 1 页,20 条) -
ZRANGEBYSCORE seckill:goods:list:1001 0 1000 LIMIT 20 20(按热度分页)
每次库存变更(预扣减/下单成功/超时回滚)都同步更新对应商品在 ZSET 中的 score(比如用剩余库存倒序),保证用户看到的“热销榜”和“库存告急”提示是准实时的。GORM 在这里只用于初始化写入和异步落库校验,不参与分页响应。
GORM 分页查询必须禁用 Offset,改用游标分页
如果真要从 MySQL 查(比如管理后台导出全量),绝不能用 Offset。GORM 的 Scopes 可封装游标逻辑:
func CursorPaginate(db *gorm.DB, lastID uint64, limit int) *gorm.DB {
return db.Where("id > ?", lastID).Order("id ASC").Limit(limit)
}
// 调用
var goods []Goods
db.Scopes(CursorPaginate(db, 12345, 50)).Find(&goods)
要点:lastID 必须是上一页最后一条的主键值,且该字段要有索引;limit 建议 ≤ 100;游标方式天然规避了深分页性能坍塌,但要求数据插入顺序稳定(不能用 UUID 主键)。
Hertz 中间件要提前拦截非秒杀时段的分页请求
秒杀商品列表不是全天可查。Hertz 的中间件应在路由层就过滤掉无效请求,避免进业务逻辑:
- 检查
activityId是否存在且状态为PREHEAT或ONGOING,否则直接c.AbortWithStatusJSON(404, "活动未开始") - 对
/goods?activity_id=1001&page=1这类带参数的请求,提取activity_id后查 Redis 缓存seckill:activity:1001:status,命中才放行 - 禁止前端传
sort=price等动态排序——所有排序规则必须预定义并固化在 Redis ZSET 的 score 更新逻辑里
最易被忽略的是缓存穿透防护:当 activity_id 是非法值(如负数、超长字符串),Redis 无对应 key,必须用布隆过滤器或空值缓存(seckill:activity:-999:status → "INVALID", TTL 1min)挡掉,否则恶意刷接口会穿透到 GORM 层触发全表扫描。











