gin 本身不串行处理请求,抢购“排队感”源于业务阻塞或资源争抢;应避免全局锁、同步阻塞操作,推荐 redis+lua 原子扣库存,用消息队列解耦下单流程,确保 handler 轻量快速响应。

Gin 本身不串行处理请求,抢购场景下出现“排队感”,基本是业务逻辑或资源层阻塞导致的,不是框架问题。
为什么抢购接口看起来像在排队?
常见现象:多个并发请求进来,日志时间戳挨得很近,但响应延迟逐个拉长,像在排队。这不是 Gin 在排队,而是以下几种情况之一:
-
time.Sleep、同步数据库写入、未设超时的http.Post等阻塞操作,让 goroutine 卡住,虽不影响其他请求,但终端日志刷出慢,造成视觉排队 - 共享资源争抢:比如用全局
*sql.DB且没调db.SetMaxOpenConns(),所有请求挤在一个连接池里等空闲连接 - 库存扣减逻辑用了
sync.Mutex或redis.LOCK但锁粒度太大(例如整个商品ID一把锁),导致高并发下大量 goroutine 阻塞等待 - 客户端复用 HTTP/1.1 连接,curl -N 或浏览器连续发请求,服务端其实是并发处理的,但日志按接收顺序打,看起来像串行
库存扣减必须原子,但别锁整条路由
抢购核心是“减库存 + 下单”原子性,但锁范围要小——不能对整个 /buy 路由加锁,也不能对所有商品共用一个锁。
- 推荐用 Redis + Lua:把库存 key 设为
stock:{sku_id},用EVAL执行一段脚本,先GET再DECR再判断是否 ≥0,整个过程在 Redis 单线程内完成,无竞态 - 避免用 Go 层
sync.Map或map + mutex存库存,它只在单机有效,且无法跨 goroutine 安全读写高频变更的数值 - 如果必须用 DB,确保
UPDATE stock SET qty = qty - 1 WHERE sku_id = ? AND qty > 0,靠数据库行锁和影响行数判断是否扣成功,不要先SELECT再UPDATE
排队提示该由前端还是后端做?
真实排队(如秒杀队列)不该靠后端“等”来实现,而应快速响应“已入队”或“已售罄”,把压力卸到异步层。
- 不要在 handler 里
time.Sleep(500 * time.Millisecond)模拟排队——这会吃光 goroutine,拖垮整个服务 - 正确做法:收到请求立刻校验库存,有货就扣减并投递到消息队列(如
go-redis的RPUSH),返回{"status":"queued","order_id":"xxx"};后台消费者再异步建单、通知、发券 - 前端拿到 “queued” 后轮询或 WebSocket 监听结果,后端保持 handler 轻量、无等待、毫秒级返回
真正难的不是写个 POST /buy,而是库存一致性、分布式锁选型、失败回滚路径、以及如何不让用户看到“502”——这些细节漏掉一个,高并发下就会雪崩。











