秒杀接口必须用gin而非net/http,因其基于radix树实现o(log n)路由匹配,可毫秒级响应上万路径;而net/http线性匹配在千级路由时即成性能瓶颈。

秒杀接口为什么必须用 Gin 而不是原生 net/http
因为 net/http 默认路由是线性匹配,1000 个商品路由就遍历 1000 次;而 Gin 的 Engine 基于 Radix 树,路由查找时间复杂度是 O(log n),哪怕上万条路径也能毫秒级响应。秒杀场景下每秒数万请求打到同一个接口,路由性能瓶颈最先暴露——这不是“够用就行”,而是“不用 Gin 就扛不住”。
如何用 Gin 中间件拦截无效秒杀请求
真实秒杀流量里,70% 以上是无效请求:未登录、token 过期、IP 频繁刷、非活动时间访问。靠业务层 if-else 判断会浪费 CPU 和数据库连接。
- 用
gin.BasicAuth()或自定义 JWT 中间件校验Authorizationheader,失败直接c.AbortWithStatusJSON(401, ...) - 在中间件里查 Redis:
GET seckill:activity:20260812:status,值不是running就拒掉 - 对同一用户 ID 做
INCR seckill:user:10086:rate+EXPIRE,超阈值(比如 5 次/秒)返回429 Too Many Requests
注意:所有中间件里的 Redis 操作必须用连接池(如 redis.NewPool),不能每次新建连接,否则秒杀开始瞬间就会耗尽文件描述符。
库存扣减为什么不能只靠 GORM Update
用 db.Model(&product).Where("id = ? AND stock > 0", id).Update("stock", gorm.Expr("stock - 1")) 看似原子,但 MySQL 在高并发下仍可能超卖——因为 WHERE 条件判断和 UPDATE 执行之间存在微小时间窗口,多个事务同时读到 stock=1,都通过判断,最终 stock 变成 -1。
- 必须配合 Redis 原子操作:先
DECR seckill:product:1001,返回值 ≥ 0 才走后续逻辑 - 若 Redis 返回 -1,说明库存已空,直接返回
{"code": 400, "msg": "库存不足"},不碰数据库 - Redis 扣减成功后,再异步写 MySQL 订单和库存变更(用消息队列或 goroutine + channel),避免阻塞 HTTP 响应
别忘了 Redis 扣减前要 WATCH seckill:product:1001 并用 MULTI/EXEC 包裹,否则在 pipeline 场景下仍有竞态风险。
为什么 Gin 的 Context 不能跨 goroutine 传递
常见错误是在秒杀 handler 里起 goroutine 处理异步任务,然后把 *gin.Context 传进去,比如:
go func(c *gin.Context) {
// ...
c.JSON(200, result)
}(c)
这会导致 panic:Gin 的 Context 内部持有 responseWriter 引用,而 response 已在主 goroutine 返回时 flush 关闭。一旦异步 goroutine 尝试写响应,就会触发 http: response.WriteHeader on hijacked connection 错误。
正确做法是只传原始数据(如 userID, productID, orderNo),异步逻辑里重新构造独立的 DB/Redis 客户端,结果写入 MQ 或回调通知,绝不触碰原 c。
真正容易被忽略的是 context 超时控制——秒杀接口必须设 c.Request.Context().Done() 监听,否则 goroutine 泄漏会拖垮整个服务。











