不能直接用 sync.mutex 因其导致请求串行阻塞、超时重试、goroutine 堆积和 oom;应改用固定容量 buffered channel 实现毫秒级准入控制与快速失败。

为什么不能直接用 sync.Mutex 做秒杀排队
秒杀场景下,用户请求瞬间涌进,如果只靠 sync.Mutex 全局锁,所有请求会串行排队,但实际中你根本等不到锁释放——超时、重试、网关拦截早把请求干掉了。更糟的是,锁持有时间不可控(比如 DB 写入慢),导致大量 goroutine 堆积在 Lock() 上,内存暴涨甚至 OOM。
真正需要的不是“加锁顺序”,而是“可控的准入队列 + 快速失败反馈”。Gin 本身不提供排队能力,得自己搭轻量级缓冲层。
- 别在 HTTP handler 里做耗时加锁,必须把排队逻辑前置到最外层
- 避免用
time.Sleep模拟等待,真实场景下用户不能卡住 - Redis 的
LPUSH + LLEN不适合高并发排队,LLEN是 O(N),且无法原子限长
用 buffered channel 实现毫秒级准入控制
核心思路:用固定容量的带缓冲 channel 当作“数字门票池”,每个秒杀名额对应一个空位。goroutine 尝试 select 非阻塞写入,成功即获得排队资格;失败则立刻返回“已售罄”。
注意 channel 容量必须严格等于库存数,且初始化后不再修改。不要用 make(chan struct{}, 100) 然后动态增减,那会破坏原子性。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
var seckillQueue = make(chan struct{}, 100) // 库存100份
<p>// 初始化:填满通道(相当于预发100张入场券)
for i := 0; i </p>
- channel 类型必须是
struct{},零内存开销 - 初始化填满后,
len(seckillQueue)就是剩余排队名额,可暴露给前端做倒计时参考 - Gin handler 中用
select { case seckillQueue 实现无锁快速判断
如何让排队成功的用户不丢失上下文
拿到 channel 入口只是第一步,后续还要扣库存、写订单、发 MQ。这些操作失败了,必须把“入场券”还回去,否则库存就永久少一张。
不能依赖 defer 或 panic 恢复,因为 handler 可能提前 return。必须显式归还:
func handleSeckill(c *gin.Context) {
select {
case
- 归还操作必须是非阻塞的(但这里 channel 永远有空位,所以安全)
- 如果业务逻辑里调用了外部服务(如 Redis 扣减),需确保幂等,否则重复归还 + 重复扣减会出错
- 不要在归还前加日志或耗时操作,防止 channel 再次被占满导致归还不上
为什么不用 Redis + Lua 做排队而选 channel
单机部署时,buffered channel 是最优解:无网络开销、无序列化成本、goroutine 调度由 runtime 保证公平。Redis 方案看似“分布式”,但引入了额外延迟(RTT ≥ 1ms)、连接池竞争、Lua 脚本复杂度,且无法解决单机瞬时压测瓶颈。
只有当服务横向扩展到多实例,且必须全局排队时,才考虑 Redis。此时务必用 SET key value EX seconds NX 做分布式锁配合 zset 排队,而不是 INCR —— 后者无法回滚。
- channel 方案天然不支持多实例,别强行加 Redis 做“伪分布式”
- 如果你用 K8s,优先通过 HPA + 单实例扩容,而非一上来就搞分布式排队
- 真正难的不是排队,是排队后那一连串异步操作的最终一致性保障
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










